0、环境说明

1、下文中跨域实现为服务器域名 http://yaogy.jd.com 向本地项目 leo.com 发起跨域请求,本地进行debug。

2、本地项目 Spring 版本为 4.3.0。

跨域的实现方式有很多种,注解、过滤器、拦截器都能很好的实现跨域的功能,但在实际应用中却发现在同一个跨域实现、同一个 controller 类下,有的跨域请求成功,有的跨域请求返回 403,如图1 所示。

image

1、基于拦截器的跨域403响应

图1所示请求中,采取的是通过过滤器的方式实现跨域,ajax 请求方式为 GET请求,content-type 为 application/json,是一个复杂请求。初始过滤器代码如下:

public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String originUrl = request.getHeader("origin");
if(!StringUtils.isEmpty(originUrl)){
//自定义跨域域名检查
boolean isAllow = checkAllow(originUrl);
if (isAllow) {
response.setHeader("Access-Control-Allow-Origin", originUrl);
}
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
response.setHeader("Access-Control-Max-Age", "1800");//30分钟
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, content-type");
}
filterChain.doFilter(request, response);
}
}

上述拦截器在拦截普通跨域请求时能够正常跨域,但遇到复杂跨域请求时,在发起预请求的时候预请求阶段就返回 403 ,跨域失败,结果如图1所示。

image

如图2所示,该方法的全限定名为 org.springframework.web.cors.DefaultCorsProcessor#processRequest,对于CorsConfiguration对象为空的预请求,将直接返回403,至于CorsConfiguration,可以参考另一篇文章

大致流程为:Spring 容器在启动的时候会扫描每一个添加了 @Controller 注解的类、@RequestMapping注解的方法,之后判断类或者方法上是否有 @CrossOrigin 注解,并将 @CrossOrigin 注解中的内容转换成 CorsConfiguration 对象,具体转换逻辑如图3所示:

image

而对于基于过滤器实现的跨域,没有 @CrossOrigin 注解的加持,CorsConfiguration 对象自然为空,而在Spring对跨域请求的处理逻辑中,对于CorsConfiguration 对象为空的预请求是会执行 rejectRequest 方法,也就是返回状态码 403。既然 Spring 对跨域请求的处理逻辑我们无法改变,所以我们可以在过滤器中添加对 预请求的单独处理或者采用注解的方式解决复杂请求的跨域403响应。修改后的过滤器如下:

public class CorsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String originUrl = request.getHeader("origin");//请求的地址
if(!StringUtils.isEmpty(originUrl)){
//自定义跨域域名检查
boolean isAllow = checkAllow(originUrl);
if (isAllow) {
response.setHeader("Access-Control-Allow-Origin", originUrl);
}
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
response.setHeader("Access-Control-Max-Age", "1800");//30分钟
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, content-type");
//对预请求单独处理
String method = request.getMethod();
if (method.equalsIgnoreCase("OPTIONS")){
response.setStatus(HttpServletResponse.SC_OK);
return;
}
}
filterChain.doFilter(request, response);
}
}

2、基于注解的跨域403响应

基于注解的跨域实现能够解决 CorsConfiguration 对象为空的问题,进而解决了在拦截器的实现方式中预请求403的问题。但注解并不能解决所有问题,注解使用不当的时候仍然可能返回403响应。

问题产生场景:

1、方法上的注解未设置 methods 属性

2、ajax请求方法为 POST(或其他非HEAD、GET)方法。

问题产生原因:

image

如图4所示,当方法上的 @CrossOrigin 注解未进行任何配置时,获得的 allowMethods 对象为空导致返回 403 响应。checkMethods方法代码如下:

protected List checkMethods(CorsConfiguration config, HttpMethod requestMethod) {
//方法的具体逻辑见图5
return config.checkHttpMethod(requestMethod);
}
image

由图5可知,当@CrossOrigin 注解未配置 methods 属性时,默认只允许 GET、HEAD 方法的访问,对于其他的请求方法都将返回403响应。

解决办法:

给方法添加跨域注解时增加需要支持的方法,比如:@CrossOrigin(methods = {RequestMethod.GET, RequestMethod.POST})

3、总结

a、使用过滤器、拦截器等的配置方式无法解决复杂请求的预请求的问题,但对于POST方法的简单请求不会出现问题。

b、使用注解的方式在不设置跨域方法的情况下对非 GET、HEAD 方法的请求会出现403的响应,但对于复杂请求无需做额外的逻辑处理。