跨域和options请求

前后端分离的项目中,前端和后端单独部署,使用不同的域名,前端代码在浏览器端访问后端的时候就会有跨域问题。

之前帮前端调试活动页面的时候,chrome调试工具上总是看到一个请求会重复发两次,后端加了锁,并做了数据校验,所以前端不管怎么搞都不会有问题,也没在意,一直以为是前端代码的问题。

后来研究跨域的时候,发现前端发的两个请求中,第一个是  options请求,第二个才是正常的 get/post请求。options请求是跨域试探,试探通过后才会发送真正的业务请求。

1. 浏览器端跨域处理的流程

整个跨域过程主要是浏览器在处理,服务器端也要感知跨域,但是主要是告诉浏览器跨域的相关的配置,比如哪些域名被允许,哪些请求头被允许

跨域和options请求_第1张图片

(1)  发送 options请求的条件

当请求满足下述任一条件时,即应首先发送预检请求:

  • 使用了下面任一 HTTP 方法:
  • PUT
  • DELETE
  • CONNECT
  • OPTIONS
  • TRACE
  • PATCH
  • 人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type (but note the additional requirements below)
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width
  • Content-Type 的值不属于下列之一:
  • application/x-www-form-urlencoded
  • (1)  发送 options请求的条件multipart/form-data
  • text/plain

开发中一般使用的都是 get /post 请求,一般不满足第一个条件

但是 请求的 content-type 一般为 application / json,符合第二个条件

 

(2)  Access-Control-Request-Headers

用 jquery 的 ajax请求的时候,第一次的options请求会加上下面的请求头

这个请求头的内容是正式请求时会发送的自定义请求,也就是说jquery的ajax在跨域的时候加了 x-csrf-token这个自定义请求头,然后浏览器监控到了x-csrf-token这个自定义请求头,就在options请求的 Acess-Control-Request-Headers的请求头内容中加入了 x-csrf-token

    var formData = new FormData();
    var xhr = new XMLHttpRequest();
    xhr.timeout = 30000;
    xhr.responseType = "json";
    xhr.open('POST', 'http://localhost:8090/springProject/testController1/upLoad1', true);
    xhr.setRequestHeader("content-type", "application/json");
    xhr.setRequestHeader("userName", "admin");
    xhr.setRequestHeader("passWord", "1111");
    xhr.send();

上面的代码,我加了一个  userName: admin    passWord: 1111  的请求头,浏览器发送的options请求的请求头参数如下

可以看到有一个  Access-Control-Request-Headers: username,  password

跨域和options请求_第2张图片

Jquery的ajax由于加了自定义请求头,所以会先发送options请求

 

(3)  浏览器校验后端responseHeader返回的跨域配置

Access-Control-Allow-Origin: 允许哪些域被允许跨域,例如 http://qq.com 或 https://qq.com,或者设置为 * ,即允许所有域访问(通常见于 CDN )
Access-Control-Allow-Credentials: 是否携带票据访问(对应 fetch 方法中 credentials),当该值为 true 时,Access-Control-Allow-Origin 不允许设置为 *
Access-Control-Allow-Methods: 标识该资源支持哪些方法,例如:POST, GET, PUT, DELETE
Access-Control-Allow-Headers: 标识允许哪些额外的自定义 header 字段和非简单值的字段(这个后面会解释)
Access-Control-Max-Age: 表示可以缓存 Access-Control-Allow-Methods 和 Access-Control-Allow-Headers 提供的信息多长时间,单位秒,一般为10分钟。
Access-Control-Expose-Headers: 通过该字段指出哪些额外的 header 可以被支持。

后端可以返回上面这些跨域配置相关的字段,浏览器根据后端返回的配置进行校验

2. 后端处理跨域的过程

(1) debug准备

在 tomcat 8.5.34      Spring 5.1.4 的环境下

一开始项目下面没有 tomcat 的包,IDEA中debug到tomcat包里面的方法进不去,pom.xml加上 tomcat的依赖就好了,注意版本要和使用的tomcat版本一致,不然debug的时候代码行数对不上


      org.apache.tomcat
      tomcat-catalina
      8.5.34
    

tomcat-catalina.8.5.34.jar里面已经有 servlet相关的包了,我开始还加了下面javax-servlet-api-3.1.0.jar这个包,debug的时候会跳到javax-servlet-api-3.1.0.jar里面了,代码行数对不上。去掉javax-servlet-api-3.1.0.jar 的依赖后,debug就会跳到

tomcat-catalina.8.5.34.jar的包里面


      javax.servlet
      javax.servlet-api
      3.1.0
      provided
    

(2) 源码分析

看源码的话,后端只是处理了options请求,处理options请求时会判断  responseHeader中是否有 Access-Control-Allow-Header,有的话基本就放过了,没有就拒绝请求,返回403

 

javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)

将不同的http请求方法映射到对应的java方法

protected void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {

        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                // servlet doesn't support if-modified-since, no reason
                // to go through further expensive logic
                doGet(req, resp);
            } else {
                long ifModifiedSince;
                try {
                    ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                } catch (IllegalArgumentException iae) {
                    // Invalid date header - proceed as if none was set
                    ifModifiedSince = -1;
                }
                if (ifModifiedSince < (lastModified / 1000 * 1000)) {
                    // If the servlet mod time is later, call doGet()
                    // Round down to the nearest second for a proper compare
                    // A ifModifiedSince of -1 will always be less
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }

        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);

        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);

        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);

        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);

        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);

        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);

        } else {
            //
            // Note that this means NO servlet supports whatever
            // method was requested, anywhere on this server.
            //

            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);

            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }

 

org.springframework.web.cors.DefaultCorsProcessor#processRequest

处理跨域的options请求主要是这个方法,没有跨域 或者 response中有 Access-Control-Allow-Origin,都直接返回true

CorsConfiguration是跨域配置,应该是可以利用这个对象配置跨域的规则

public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
			HttpServletResponse response) throws IOException {

		if (!CorsUtils.isCorsRequest(request)) {
			return true;
		}

		ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
		if (responseHasCors(serverResponse)) {
			logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
			return true;
		}

		ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
		if (WebUtils.isSameOrigin(serverRequest)) {
			logger.trace("Skip: request is from same origin");
			return true;
		}

		boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
		if (config == null) {
			if (preFlightRequest) {
				rejectRequest(serverResponse);
				return false;
			}
			else {
				return true;
			}
		}

		return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
	}

如果要设置跨域配置,最简单的办法就是自己写一个过滤器,在前置方法中往responseHeader里面设置参数就好了

public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, WG-App-Version, WG-Device-Id, WG-Network-Type, WG-Vendor, WG-OS-Type, WG-OS-Version, WG-Device-Model, WG-CPU, WG-Sid, WG-App-Id, WG-Token, x-csrf-token");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET");
        response.setHeader("Access-Control-Allow-Credentials", "true");

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

 

你可能感兴趣的:(Java基础)