SpringMVC跨域Cors

# 问题背景


  测试环境有两个接口需要提供给别的项目的前端调用,前端项目运行地址为http://a.com,接口所在服务器地址为http://b.com; 总所周知,受同源策略的影响,浏览器执行ajax请求时会报Origin错误;因此运维在服务器配置了跨域;具体配置如下:

server{
    #省略其他   
    map $http_origin $corsHost {
        default 0;
        "~http://a.com" http://a.com;
    }
    if ($request_method = 'OPTIONS') {
          add_header Access-Control-Allow-Origin $corsHost;
          return 204;
    }
}
1
2
3
4
5
6
7
8
9
10
11

  $http_origin为nginx内置变量(http_origin固定写法),用于获取请求头的Origin;$corsHost自定义变量(corsHost定义为abc也可) 表示匹配里面的键值对,如果Origin符合前一个正则表达式,则$corsHost就等于后面的值,如果都匹配不上,则$corsHost为default定义的0;
  if内表示如果请求的方法类型为OPTIONS,则给返回头添加允许跨域的表示(从map中匹配而来),并且直接返回204状态码;
  至此一切配置完毕,然后在测试两个接口时却发现,一个正常请求返回,另一个提示Access-Control-Allow-Origin返回了两次, 导致请求失败;错误信息如下

Access to XMLHttpRequest at 'http://localhost:8080/user' from origin 'http://localhost:63342' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values 'http://localhost:63342, http://localhost:63342', but only one is allowed.
1

  从错误信息可以得知,是因为返回服务端在返回客户端的请求头上设置了两遍Access-Control-Allow-Origin响应头导致,于是开始着手排查;

# 接口排查


  首先想到接口在返回前是否对Response对象进行了设置

//Response.add方法不会校验是否已经添加过这个响应头,但是set方法就不一样了
response.addHeader("Access-Control-Allow-Origin", "http://localhost:63342");
response.addHeader("Access-Control-Allow-Origin", "http://localhost:63342");
1
2
3

# mvc配置查询


此时在项目中全局搜索cors关键词,由于老项目在xml中搜到了如下配置

<mvc:cors>
		<mvc:mapping path="/*"/>
</mvc:cors>
1
2
3

  配置含义:对全局的请求进行拦截过滤,如果发现跨域请求,则把请求头携带的Origin放置到响应头的Access-Control-Allow-Origin中;那么疑问来了,为什么配置对/user生效(设置了两遍响应头),对/user/list接口失效;

  那就必须断点排查一下,我们知道mvc请求的核心在DispatchServlet.doService方法,其中doService方法核心调用了doDispatch方法;

//通过request对象拿到处理链
HandlerExecutionChain mappedHandler = this.getHandler(processedRequest);


if (!mappedHandler.applyPreHandle(processedRequest, response)) {
   return;
}
//真实处理
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
1
2
3
4
5
6
7
8
9

处理链中如何添加cors handler呢?

HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
//判断请求是否跨域 标准request header中 Origin不为空
if (CorsUtils.isCorsRequest(request)) {
//从全局配置中找
                CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
                CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, request);
                CorsConfiguration config = globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig;
                executionChain = this.getCorsHandlerExecutionChain(request, executionChain, config);
}
1
2
3
4
5
6
7
8
9

那么怎么从全局配置中能拿到跨域配置呢?

public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
        //此处的corsConfigurations是重点 
        //其结构private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap(); 
        //从定义上为xml解析而来,key为path,CorsConfiguration为配置的mvc:cors配置的其他属性
        Iterator var3 = this.corsConfigurations.entrySet().iterator();
        Entry entry;
        do {
            if (!var3.hasNext()) {
                return null;
            }
            entry = (Entry)var3.next();
        } while(!this.pathMatcher.match((String)entry.getKey(), lookupPath));

        return (CorsConfiguration)entry.getValue();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

首先聚焦chain的applyPreHandle方法

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
  			//就断点处而言出现两个handler:
  			//org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor
  			//org.springframework.web.servlet.handler.AbstractHandlerMapping.CorsInterceptor
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
              //主要看corsInterceptor看下面处理
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(request, response, this.handler)) {
                    this.triggerAfterCompletion(request, response, (Exception)null);
                    return false;
                }
            }
        }

        return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

其中CorsInterceptor的prehandle方法

public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (!CorsUtils.isCorsRequest(request)) {
            return true;
        } else {
            ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
            if (this.responseHasCors(serverResponse)) {
                logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
                return true;
            } else {
                ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
              //判断是否同源 获取request的协议、host、端口与Origin作对比
                if (WebUtils.isSameOrigin(serverRequest)) {
                    logger.debug("Skip CORS processing: request is from same origin");
                    return true;
                } else {
                    boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
                    if (config == null) {
                        if (preFlightRequest) {
                            this.rejectRequest(serverResponse);
                            return false;
                        } else {
                            return true;
                        }
                    } else {
                        return this.handleInternal(serverRequest, serverResponse, config, preFlightRequest);
                    }
                }
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

其中handleInternal方法具体处理过程

protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
        String requestOrigin = request.getHeaders().getOrigin();
        String allowOrigin = this.checkOrigin(config, requestOrigin);
        HttpMethod requestMethod = this.getMethodToUse(request, preFlightRequest);
        List<HttpMethod> allowMethods = this.checkMethods(config, requestMethod);
        List<String> requestHeaders = this.getHeadersToUse(request, preFlightRequest);
        List<String> allowHeaders = this.checkHeaders(config, requestHeaders);
        if (allowOrigin == null || allowMethods == null || preFlightRequest && allowHeaders == null) {
            this.rejectRequest(response);
            return false;
        } else {
            HttpHeaders responseHeaders = response.getHeaders();
            responseHeaders.setAccessControlAllowOrigin(allowOrigin);
            responseHeaders.add("Vary", "Origin");
            if (preFlightRequest) {
                responseHeaders.setAccessControlAllowMethods(allowMethods);
            }

            if (preFlightRequest && !allowHeaders.isEmpty()) {
                responseHeaders.setAccessControlAllowHeaders(allowHeaders);
            }

            if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
                responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
            }

            if (Boolean.TRUE.equals(config.getAllowCredentials())) {
                responseHeaders.setAccessControlAllowCredentials(true);
            }
						//默认maxAge如果不配置为1800s=30min
            if (preFlightRequest && config.getMaxAge() != null) {
                responseHeaders.setAccessControlMaxAge(config.getMaxAge());
            }

            response.flush();
            return true;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 时序性分析


  spring除了提供xml配置之外,同样提供了基于注解的配置@CrossCors;可以注解在类或者方法上,是一种更细粒度的配置;由于基于注解的方式和基础xml配置的方式相同,SpringMVC把其处理细节包装在getHandler(processedRequest);中,具体逻辑可以简述为如果请求是预检请求,则返回PreFlightHander;否则在拦截器末尾添加一个CorsInterceptor;因此跨域拦截器滞后于自定义的权限校验拦截,如果权限拦截提前返回,将导致跨域错误提示;