SpringMVC跨域问题排查以及源码实现
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配置转发请求到后端服务,也就避开了跨域的问题。所以为了不影响线上环境,先暂时把线上的调用改成和线下一致,服务正常。
后面开始排查具体的失败问题,开始排查的几个点:
- 4.1.1到4.3.10 springmvc有什么版本更新
- 为什么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的拦截器。
通过上面的排查,也就有了后面的问题和源码解读。
- 为什么4.1.1和4.3.10的options请求拦截器处理不一样。
- 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
出处: