Ajax请求成功却执行error回调方法(Ajax跨域请求问题)

遇到的问题描述:

Ajax使用post请求添加分组,请求url是RESTful风格。添加成功了已经向数据库写入了信息,但是Ajax却执行了error回调,观察网络发现有两次相同请求一次是get请求,一次是post请求携带了需要向数据库写入的参数。第一次请求成功返回200,第二次却返回404,所以导致回调进入了error。
于是乎查找了很久解决方案,明白了是跨域请求导致,而Ajax只能同源请求。

解决方案:

写一个自定义过滤器

package xyz.guqing.taotao.manage.filter;
 
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
 
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
 
/**
 * @ClassName:  CrossFilter   
 * @Description: 跨域请求过滤器 
 * @author: guqing
 * @date:   2019年1月4日 下午5:33:52   
 *
 */
public class CrossFilter implements Filter {
	private static final boolean debug = true;
	private FilterConfig filterConfig = null;
	
	public CrossFilter() {
		super();
	}
 
	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		this.filterConfig = filterConfig;
        if (filterConfig != null) {
            if (debug) {                
                log("CrossFilter:Initializing filter");
            }
        }
 
	}
	
	 @Override
    public String toString() {
        if (filterConfig == null) {
            return ("CrossFilter()");
        }
        StringBuffer sb = new StringBuffer("CrossFilter(");
        sb.append(filterConfig);
        sb.append(")");
        return (sb.toString());
    }
 
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		if (debug) {
			log("CrossFilter:doFilter()");
        }
 
         if(response instanceof HttpServletResponse){
        	 HttpServletResponse alteredResponse = ((HttpServletResponse)response);
	        // I need to find a way to make sure this only gets called on 200-300 http responses
	        // TODO: see above comment
        	 addHeadersFor200Response(alteredResponse);
         }
         doBeforeProcessing(request, response);
 
         Throwable problem = null;
         try {
             chain.doFilter(request, response);
         } catch (Throwable t) {
             // If an exception is thrown somewhere down the filter chain,
             // we still want to execute our after processing, and then
             // rethrow the problem after that.
             problem = t;
             t.printStackTrace();
         }
 
         doAfterProcessing(request, response);
 
         // If there was a problem, we want to rethrow it if it is
         // a known type, otherwise log it.
         if (problem != null) {
             if (problem instanceof ServletException) {
                 throw (ServletException) problem;
             }
             if (problem instanceof IOException) {
                 throw (IOException) problem;
             }
             sendProcessingError(problem, response);
         }
       
 
	}
 
	@Override
	public void destroy() {
 
	}
	
	private void doBeforeProcessing(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
        if (debug) {
            log("CrossFilter:DoBeforeProcessing");
        }
 
    }    
 
    private void doAfterProcessing(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
        if (debug) {
        	log("CrossFilter:DoAfterProcessing");
        }
    }
    
    private void addHeadersFor200Response(HttpServletResponse response){
        //TODO: externalize the Allow-Origin
        response.addHeader("Access-Control-Allow-Origin", "*");
        response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, HEAD");
        response.addHeader("Access-Control-Allow-Headers", "X-PINGOTHER, Origin, X-Requested-With, Content-Type, Accept");
        response.addHeader("Access-Control-Max-Age", "1728000");
    }
    
    private void sendProcessingError(Throwable t, ServletResponse response) {
        String stackTrace = getStackTrace(t);        
 
        if (stackTrace != null && !stackTrace.equals("")) {
            try {
                response.setContentType("text/html");
                PrintStream ps = new PrintStream(response.getOutputStream());
                PrintWriter pw = new PrintWriter(ps);                
                pw.print("\n\nError\n\n\n"); //NOI18N
 
                // PENDING! Localize this for next official release
                pw.print("

The resource did not process correctly

\n
\n");                
                pw.print(stackTrace);                
                pw.print("
\n"); //NOI18N pw.close(); ps.close(); response.getOutputStream().close(); } catch (Exception ex) { } } else { try { PrintStream ps = new PrintStream(response.getOutputStream()); t.printStackTrace(ps); ps.close(); response.getOutputStream().close(); } catch (Exception ex) { } } } public static String getStackTrace(Throwable t) { String stackTrace = null; try { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); t.printStackTrace(pw); pw.close(); sw.close(); stackTrace = sw.getBuffer().toString(); } catch (Exception ex) { } return stackTrace; } public void log(String msg) { filterConfig.getServletContext().log(msg); } }

并在web.xml中配置



	crossFilter
	xyz.guqing.taotao.manage.filter.CrossFilter


	crossFilter
	/*

其他解决方案及一些概念解释见下方(文字有点多)!
使用此过滤器以后请求信息为:(详细解释见下方文字)

Response Headers
Access-Control-Allow-Headers: X-PINGOTHER, Origin, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE, HEAD
Access-Control-Allow-Origin: *
Access-Control-Max-Age: 1728000
Connection: keep-alive
Content-Length: 0
Date: Fri, 04 Jan 2019 09:44:06 GMT
Server: nginx/1.15.8

Request Headers
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Content-Length: 275
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: JSESSIONID=DB81C8BDC206CD086752810A3DE200C0
Host: localhost
Origin: http://localhost
Referer: http://localhost/admin/page/index
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
X-Requested-With: XMLHttpRequest

概念解释

什么是同源策略?

同源策略(Same origin policy),它是由Netscape提出的一个著名的安全策略。
现在所有支持JavaScript 的浏览器都会使用这个策略。
所谓同源是指:域名,协议,端口相同。
比如:当一个浏览器的两个tab标签页中分别打开百度和谷歌的页面那么当浏览器的百度tab页执行一个脚本的时候会检查这个脚本是属于哪个页面的,即检查是否同源,只有和百度同源的脚本才会被执行。如果非同源,那么在请求数据时,浏览器会在控制台中报一个异常,提示拒绝访问。

同源策略控制了不同源之间的交互、交互通常分为三类:

  • 允许进行跨域写操作(cross-origin writes):例如链接(links)、重定向以及表单提交
  • 允许跨域资源嵌入(cross-origin embedding)
  • 不允许跨域读操作(cross-origin reads):但常可以通过内嵌资源来巧妙的进行读取访问

同源政策的目的:
是为了保证用户信息的安全,防止恶意的网站窃取数据。
设想这样一种情况:A网站是一家银行,一用户登录A网站以后,又去浏览其他网站。如果其他网站可以读取A网站的Cookie,会发生什么?
很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。

同源政策限制范围:
(1) Cookie、LocalStorage 和 IndexDB 无法读取。
(2) DOM 无法获得。
(3) AJAX 请求不能发送。

解决Ajax请求跨域问题:

不同源即跨域,可以使用CORS和JSONP来解决跨域请求问题,但是JSONP只支持GET请求。
使用JSONP:


$.ajax({
     type:'get',
     url: url ,
     dataType:'jsonp',
     success:function(data){
        console.log(data);
     },
     error:function(XMLHttpRequest, textStatus, errorThrown){
          alert(XMLHttpRequest.status);
          alert(XMLHttpRequest.readyState);
          alert(textStatus);
     }
 });

如上加上 dataType:'jsonp’即可

什么是CORS?

CORS是一个W3C标准,全程是"跨域资源共享"(Cross-origin resource sharing)
它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。
相比于jsonp只能发送get请求,CORS允许发送任何类型的请求。但CORS要求浏览器和服务器同时支持。
目前所有浏览器都支持,IE需要IE10以上。

整个CORS通讯过程中都是浏览器自动完成,不需要用户的参与。CORS通讯和同源的AJAX请求没有区别。浏览器一旦发现AJAX请求跨域,
就会自动添加一些头部信息,有时候还会多出一次附加请求。
览器将CORS请求分为两类:简单请求和非简单请求:
只要同时满足以下两个条件就是简单请求,否则就是非简单请求:
(1)请求方法是下列方法之一:

- HEAD
- GET
- POST

(2)HTTP头信息不超出一下几个字段:

- Accept
- Aceept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值application/x-www-form-urlencoded/multipart/form-data/text/plain

对于简单请求,浏览器会自动在头部信息里增加一个Origin字段,用来表示请求来于哪个源服务器根据这个值决定是否同意此次请求。如果origin不在请求范围内,服务器返回一个正常的http回应。

这个回应的头信息中没有Access-Control-Allow-Origin字段,浏览器发现没有这个字段之后就会抛出一个错误。

如果origin在请求范围内,服务器返回的响应会多出几个头信息字段,其中一个是Access-Control-Allow-Origin,它的值要么是origin的值,要么是*,表示允许任何域名的请求
对于非简单请求,它会在正式通信之前,增加一次http查询请求,成为“预检”请求(preflight)。

通常是一个OPION请求。这个请求先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪http动词和头信息字段。只有得到肯定答案,浏览器才会发出正常的XMLHTTPRequest请求,否则报错。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: 
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: 
Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin

该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS
请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也
只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
上面的例子指定,	getResponseHeader('FooBar')可以返回FooBar字段的值。

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

在服务端解决跨域请求的另一种方法:

在给客户端响应数据之前设置响应头

// 跨域配置
response.setHeader("Access-Control-Allow-Origin","*");
response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600"); // 保持跨域Ajax时的Cookie
response.setHeader("Access-Control-Allow-Headers", "x-auth-token, x-requested-with,Authorization,Origin, Accept, Content-Type,x-xsrf-token");

你可能感兴趣的:(后端技术笔记)