解读SpringBoot中addCorsMappings配置跨域与拦截器互斥问题的原因
作者:huangyaa729
SpringBoot中addCorsMappings配置跨域与拦截器互斥
如题,前两天在做前后端分离项目时,碰到了这个问题,登录token验证的拦截器使项目中配置的跨域配置失效,导致浏览器抛出跨域请求错误,跨域配置如下:
public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurerAdapter() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins(origins) .allowedHeaders("*") .allowCredentials(true) .allowedMethods("*") .maxAge(3600); } }; }
通过在网上的查询,发现了如下解释
- 但是使用此方法配置之后再使用自定义拦截器时跨域相关配置就会失效。
- 原因是请求经过的先后顺序问题,当请求到来时会先进入拦截器中,而不是进入Mapping映射中,所以返回的头信息中并没有配置的跨域信息。浏览器就会报跨域异常。
然后参考了网上给出的方法,重新引入了跨域过滤器配置,解决了这个问题。
那最终这个问题产生的原因是什么的,真的如上诉所说吗,我通过调试与研究源码,找了原因。
在springMvc中,我们都知道路径的映射匹配是通过DispatcherServlet这个类来实现的,最终的函数执行在doDispatch()这个方法中:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. (1)mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. (2)HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } (3)if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. (4)mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); (5)mappedHandler.applyPostHandle(processedRequest, response, mv); }
在这个类中,我们关注(1)-(5)这几句代码,基本上整个映射执行的逻辑就明了了:
- (1)根据请求request获取执行器链(包括拦截器和最终执行方法Handler)
- (2)根据Handler获取handlerAdapter;
- (3)执行执行器链中的拦截方法(preHandle);
- (4)执行handler方法;
- (5)执行执行器链中的拦截方法(postHandle);
在这个函数中我们并没有看到什么时候执行addCorsMappings这一配置内容,那它到底是什么时候添加的呢,那就需要仔细分析步骤(1)了:获取整个执行器链。
通过调试定位,我发现getHandle()最终执行的AbstractHandlerMapping这个类的函数
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { (1)Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { return null; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; handler = getApplicationContext().getBean(handlerName); } (2)HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); (3) executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
这个函数中我也标记了(1)、(2)、(3)这三条语句:
- (1)获取request所需执行的handler,具体逻辑不再细说,有兴趣的可以参考我的另一篇文章;
- (2)获取执行器链,简单来说就是把具体的执行器和整个拦截器链组成一个链队形,方便后续执行;
- (3)这个就是关键点,可能有的同学已经看明白了,addCorsMapping配置就是在这块引入的;
进入这个方法后,一切都明了了;
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, CorsConfiguration config) { if (CorsUtils.isPreFlightRequest(request)) { HandlerInterceptor[] interceptors = chain.getInterceptors(); chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors); } else { chain.addInterceptor(new CorsInterceptor(config)); } return chain; }
先判断request是否是预检请求(不明白什么是预检请求的可以自身搜索相关解释,很多,不再赘述),是预检请求则生成个预检执行器PreFlightHandler,然后在doDispatch函数(4)中执行;
否则生成一个跨域拦截器加入拦截器链中,最终再doDispatch函数(3)处执行,而因为拦截器是顺序执行的,如果前面执行失败异常返回后,后面的则不再执行。
所以当跨越请求在拦截器那边处理后就异常返回了,那么响应的response报文头部关于跨域允许的信息就没有被正确设置,导致浏览器认为服务不允许跨域,而造成错误;而当我们使用过滤器时,过滤器先于拦截器执行,那么无论是否被拦截,始终有允许跨域的头部信息,就不会出问题了。
另注:
对于预检请求,一般token验证时是不会拦截此请求的,因为预检请求不会附带任何参数信息,也就没有所需的token信息,所以拦截时需过滤预检请求
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。