SpringMVC跨域问题排查以及源码实现

最近一次项目中,将SpringMVC版本从4.1.1升级到4.3.10,出现跨域失败的情况。关于同源策略和跨域解决方案,网上有很多资料。

项目采用的方式是通过实现过滤器Filter,在Response返回头文件添加跨域资源共享(CORS) 相关的参数。

response.addHeader("Access-Control-Allow-Origin", "http://test.com");
response.addHeader("Access-Control-Allow-Credentials", "true");
response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD");
response.addHeader("Access-Control-Allow-Headers", "Content-Type");
response.addHeader("Access-Control-Max-Age", "3600");

发布完成,回归测试的过程发现跨域失败,但是本地开发是没有问题的。

经过排查线下和线上的区别,因为是前后端分离项目,所以线上基本会配置前端和后端独立的域名,通过跨域的方式调用。但是本地开发的时候,前端通过nginx配置转发请求到后端服务,也就避开了跨域的问题。所以为了不影响线上环境,先暂时把线上的调用改成和线下一致,服务正常。

后面开始排查具体的失败问题,开始排查的几个点:

  1. 4.1.1到4.3.10 SpringMVC有什么版本更新
  2. 为什么4.1.1没有问题,4.3.10会有问题,毕竟项目采用的跨域解决方案是比较通用(W3C标准 )的,没有涉及框架层面。

通过查看SpringMVC官方文档,从4.2.0版本开始,SpringMVC开始支持CORS跨域解决方案,主要表现是通过简单的配置,就可以支持CORS,从后面源码分析,可以看到本质还是对Response添加头文件。

https://docs.spring.io/spring/docs/4.2.0.RELEASE/spring-framework-reference/html

可以看到最快的方式实现CORS,通过xml配置

<mvc:cors>
<mvc:mapping path="/**" />
</mvc:cors>

把项目代码原先的跨域方式改成,框架提供的方案。(去掉原先的过滤器)

再一次测试发现,并没有解决问题,还是和原先的效果是一样的。说明通过配置xml的实现和原先的实现几乎是一样的。继续排查问题。

可以看到跨域预处理请求返回状态码是302,考虑到几点:3xx一般是权限问题导致、跨域预处理请求头参数不会携带参数。所以联想到Login的拦截器,通过Debug打断点的方式,确实options请求走到了拦截器里面,那也就基本确认是因为拦截器校验失败,导致的跨域预处理请求失败。后面验证了之前4.1.1的校验过程,看到options请求并没有走到Login的拦截器。

通过上面的排查,也就有了后面的问题和源码解读。

  1. 为什么4.1.1和4.3.10的options请求拦截器处理不一样。
  2. 4.2.0以上版本SpringMVC对于CORS的实现原理。

SpringMVC的入口文件DispatcherServlet,我们分为4.2.0之前和之前两个方面追溯options请求的处理过程,对于SpringMVC本身源码不详细讨论,只针对跨域相关内容。

4.2.0之前,默认情况下DispatcherServlet继承自FrameworkServlet,FrameworkServlet处理了所有的http请求,调用processRequest() 方法。我们主要看下options请求。

protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if(this.dispatchOptionsRequest) {
this.processRequest(request, response);
if(response.containsHeader("Allow")) {
return;
}
} super.doOptions(request, new HttpServletResponseWrapper(response) {
public void setHeader(String name, String value) {
if("Allow".equals(name)) {
value = (StringUtils.hasLength(value)?value + ", ":"") + RequestMethod.PATCH.name();
} super.setHeader(name, value);
}
});
}

SpringMVC提供了Boolean类型的dispatchOptionsRequest来控制是否开启对options请求的处理,默认情况下不做处理,直接调用父类的doOptions()方法。基本上没有做任何处理,就对请求返回正常的响应结果,主要是CORS预请求作为校验需要的请求头封装。

显然在4.2.0之前的版本,options请求不会进入到Login拦截器。

4.2.0之后,SpringMVC源码文件加入了很多关于CORS处理的文件,我们还是先看到FrameworkServlet对于options的请求处理。

protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
if(this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
this.processRequest(request, response);
if(response.containsHeader("Allow")) {
return;
}
} super.doOptions(request, new HttpServletResponseWrapper(response) {
public void setHeader(String name, String value) {
if("Allow".equals(name)) {
value = (StringUtils.hasLength(value)?value + ", ":"") + HttpMethod.PATCH.name();
} super.setHeader(name, value);
}
});
}

可以看到最大的区别是CorsUtils.isPreFlightRequest(request),这个静态方法是用于判断请求是否是预处理请求,显然options会返回true,所以也就会执行和其他请求一样的processRequest()方法,继续往下看。

快速查看调用路径FrameworkServlet.processRequest()->DispatcherServlet.doService()->DispatcherServlet.doDispatch()。

try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if(mappedHandler == null || mappedHandler.getHandler() == null) {
this.noHandlerFound(processedRequest, response);
return;
} HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if(isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if(this.logger.isDebugEnabled()) {
this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
} if((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
} if(!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
} mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if(asyncManager.isConcurrentHandlingStarted()) {
return;
} this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}

可以看到一个请求,会通过getHandler()方法获取处理器,在处理之前会先执行applyPreHandle(),处理所有拦截器的前置方法,所以我们可以确定的一个问题是,因为doOptions()的不同,所以options请求拦截器处理不一样。

继续看CORS的实现原理,我们看下getHandler()方法的实现。

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Iterator var2 = this.handlerMappings.iterator(); HandlerExecutionChain handler;
do {
if(!var2.hasNext()) {
return null;
} HandlerMapping hm = (HandlerMapping)var2.next();
if(this.logger.isTraceEnabled()) {
this.logger.trace("Testing handler map [" + hm + "] in DispatcherServlet with name '" + this.getServletName() + "'");
} handler = hm.getHandler(request);
} while(handler == null); return handler;
}

针对请求request,在handlerMappings这个Map中相应的处理器,在SpringMVC执行init方法时,已经预加载处理器Map。处理器实现了HandlerMapping接口的getHandler方法。看到默认AbstractHandlerMapping抽象类实现了该方法。

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = this.getHandlerInternal(request);
if(handler == null) {
handler = this.getDefaultHandler();
} if(handler == null) {
return null;
} else {
if(handler instanceof String) {
String handlerName = (String)handler;
handler = this.getApplicationContext().getBean(handlerName);
} HandlerExecutionChain executionChain = this.getHandlerExecutionChain(handler, request);
if(CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = this.getCorsConfiguration(handler, request);
CorsConfiguration config = globalConfig != null?globalConfig.combine(handlerConfig):handlerConfig;
executionChain = this.getCorsHandlerExecutionChain(request, executionChain, config);
} return executionChain;
}
}

我们主要看对于CORS的处理代码段,首先判断CORS请求。然后是对CORS配置的config处理(也就是SpringMVC提供的配置接口,包括xml、注释),主要是支持请求类型、域、缓存时长等,继续看getCorsHandlerExecutionChain()实现。

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, CorsConfiguration config) {
if(CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new AbstractHandlerMapping.PreFlightHandler(config), interceptors);
} else {
chain.addInterceptor(new AbstractHandlerMapping.CorsInterceptor(config));
} return chain;
}

对于CORS的非简单请求,主要分为预处理请求和正常请求,SpringMVC分别进行处理,把针对每个请求的处理看作是调用链,这个调用链肯定会包含拦截器,看到上面对于预处理请求的处理方式,会把调用链根据config配置重新初始化,同时把拦截器赋值进去,这样就更近一步说明,options预处理请求,会执行到Login拦截器中。针对CORS的正常请求,SpringMVC就会动态添加一个拦截器,它的主要作用就是和我们自己实现的过滤器的效果是一致的。

综上,基本知道问题发生的原因和原理,项目后面的改进方式,采用对Login拦截器,使用封装的CorsUtils.isPreFlightRequest(request)判断是不是预处理请求,如果是,就不对登录态校验。

后记:对于options的请求,为什么SpringMVC4.2.0以后,需要配置config处理,而不是直接和原先的处理一样,直接返回成功。猜测是可以通过config更丰富的配置。而不是笼统的返回跨域支持和不支持。

转载请注明出处。

作者:wuxiwei

出处:http://www.cnblogs.com/wxw16/p/10674539.html

SpringMVC跨域问题排查以及源码实现的更多相关文章

  1. Mybatis+SpringMVC实现分页查询(附源码)

    Maven+Mybatis+Spring+SpringMVC实现分页查询(附源码) 一.项目搭建 关于项目搭建,小宝鸽以前写过一篇Spirng+SpringMVC+Maven+Mybatis+MySQ ...

  2. SpringMvc跨域支持

    SpringMvc跨域支持 在controller层加上注解@CrossOrigin可以实现跨域 该注解有两个参数 1,origins  : 允许可访问的域列表 2,maxAge:飞行前响应的缓存持续 ...

  3. 关于springmvc跨域

    spingMVC 3.X跨域 关于跨域问题,主要用的比较多的是cros跨域. 详细介绍请看https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Acces ...

  4. springmvc跨域(转)

    跨域资源共享 CORS 详解  原文链接:http://www.ruanyifeng.com/blog/2016/04/cors.html   作者: 阮一峰 日期: 2016年4月12日 CORS是 ...

  5. SpringMVC视图机制详解[附带源码分析]

    目录 前言 重要接口和类介绍 源码分析 编码自定义的ViewResolver 总结 参考资料 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门bl ...

  6. springmvc跨域

    //mvc默认是text/plain;charset=ISO-8859-1@RequestMapping(value = "/xxx", produces = "appl ...

  7. SpringMvc 跨域处理

    导读 由于浏览器对于JavaScript的同源策略的限制,导致A网站(Ajax请求)不能通过JS去访问B网站的数据,于是跨域问题就出现了. 跨域指的是域名.端口.协议的组合不同就是跨域. http:/ ...

  8. SpringMVC拦截器详解[附带源码分析]

    目录 前言 重要接口及类介绍 源码分析 拦截器的配置 编写自定义的拦截器 总结 总结 前言 SpringMVC是目前主流的Web MVC框架之一. 如果有同学对它不熟悉,那么请参考它的入门blog:h ...

  9. SpringMVC异常处理机制详解[附带源码分析]

    目录 前言 重要接口和类介绍 HandlerExceptionResolver接口 AbstractHandlerExceptionResolver抽象类 AbstractHandlerMethodE ...

随机推荐

  1. 织梦dedecms后台文章搜索关键字,关键字包含文章内容的代码修改

    1.织梦dedecms后台文章搜索功能在哪里找?织梦dedecms后台-->核心-->常用操作-->所有档案列表(或)织梦dedecms后台-->核心-->内容管理--& ...

  2. 理解RNN

    摘自:https://zybuluo.com/hanbingtao/note/541458 语言模型就是这样的东西:给定一个一句话前面的部分,预测接下来最有可能的一个词是什么. 语言模型是对一种语言的 ...

  3. Day04 (黑客成长日记) 集合记录

    集合 集合:是可变的数据类型 ,它里面的数据类型必须是不可变的数据类型,无序,不重复,不同于字典,他有元素,没有键值对(编码不常用) li = set([1,2,3]) li = {'alex','w ...

  4. 查询树节点、oracle、select...start with...connect by prior...

    通过子节点向根节点追朔. select * from persons.dept start with deptid=76 connect by prior paredeptid=deptid 通过根节 ...

  5. php中测试运行的时间,从而选择得出优化程序

    对于新手来说,优化代码的习惯十分重要, 测试运行的时间,从而得出最好的一个 <?php $t1=microtime(true);   //获取程序1,开始的时间 程序1(代码...) $t2=m ...

  6. redis在linux云服务器上完整的搭建步骤

    Redis的安装 搭建环境: 华为云linux服务器 Linux系统CneterOS-7.3 SSH客户端 Xshell6 安装c语言编译环境软件如下: 安装报错 然后找到了解决方法: 安装kerne ...

  7. vscode配置git及码云

    1.将代码放到码云 到码云里新建一个仓库,完成后码云会有一个命令教程按上面的来就行了 码云中的使用教程: Git 全局设置: git config --global user.name "A ...

  8. codeforces 1066 B heater

    菜鸡只配做水题 思路就很简单嘛:肯定扩展的越靠后边越好了 0 0 1 0 1 1 0 0 假设范围是3 ,第一个1一定要选上,第2.3个肯定选3啦,越靠后边就一定能节省更多的点,没看出来和子问题有什么 ...

  9. 去掉jenkins的首页警告

    有时候jenkins首页会弹出一些升级警告之类的东西,不喜欢的直接屏蔽掉就行. 类似于这样: 在  全局安全配置里 将一下项目的勾去掉就行 应用,保存.这样就清爽多了

  10. 工作了才发现display全忘了

    CSS display属性这几天用的我头疼 人老了 健忘了 1.inline(行内元素) 是元素变成行内元素,拥有行内元素特性,共享属性,不会吃独食! 共享经济时代 inline是主导大哥 !impo ...