SpringMVC 与权限拦截器冲突导致的 Cors 跨域设置失效问题

本贴最后更新于 1194 天前,其中的信息可能已经时移世异

————————————————
版权声明:本文为 CSDN 博主「huangyaa729」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/huangyaa729/article/details/103893660

如题,在做前后端分离项目时,碰到了这个问题,登录 token 验证的拦截器使项目中配置的跨域配置失效,导致浏览器抛出跨域请求错误,跨域配置如下:

public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                        registry.addMapping("/**")
                        .allowedOrigins(origins)
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .allowedMethods("*")
                        .maxAge(3600);
            }
        };
    }

通过在网上的查询,发现了如下解释

但是使用此方法配置之后再使用自定义拦截器时跨域相关配置就会失效。
原因是请求经过的先后顺序问题,当请求到来时会先进入拦截器中,而不是进入 Mapping 映射中,所以返回的头信息中并没有配置的跨域信息。浏览器就会报跨域异常。参见 https://blog.csdn.net/weixin_33958585/article/details/88678712

然后参考了网上给出的方法,重新引入了跨域过滤器配置,解决了这个问题。

private CorsConfiguration corsConfig() {
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    * 请求常用的三种配置,*代表允许所有,当时你也可以自定义属性(比如header只能带什么,只能是post方式等等)
    */
    corsConfiguration.addAllowedOrigin("*");
    corsConfiguration.addAllowedHeader("*");
    corsConfiguration.addAllowedMethod("*");
    corsConfiguration.setAllowCredentials(true);
    corsConfiguration.setMaxAge(3600L);
    return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfig());
    return new CorsFilter(source);
}

那最终这个问题产生的原因是什么的,真的如上诉所说吗,我通过调试与研究源码,找了原因。

在 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,具体逻辑不再细说,有兴趣的可以参考我的另一篇博客 https://blog.csdn.net/huangyaa729/article/details/87862418;
(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)处执行,而因为拦截器是顺序执行的,如果前面执行失败异常返回后,后面的则不再执行。所有的拦截器的 preHandle() 方法的执行都在实际 Handler 的方法(比如某个 API 对应的业务方法)之前,其中任意拦截器返回 false 都会跳过后续所有处理过程。而 SpringMVC 对预检请求的处理则在 PreFlightHandler.handleRequest() 中处理,在整个处理链条中出于后置位。由于预检请求中不带 Cookie,因此先被权限拦截器拦截。
所以当跨越请求在拦截器那边处理后就异常返回了,那么响应的 response 报文头部关于跨域允许的信息就没有被正确设置,导致浏览器认为服务不允许跨域,而造成错误;而当我们使用过滤器时,过滤器先于拦截器执行,那么无论是否被拦截,始终有允许跨域的头部信息,就不会出问题了。

另注:对于预检请求,一般 token 验证时是不会拦截此请求的,因为预检请求不会附带任何参数信息,也就没有所需的 token 信息,所以拦截时需过滤预检请求。由于预检请求不会包含 Cookie 信息(浏览器本身的实现决定其是否发送 Cookie,前端无法控制,并且 Chrome 是不发送的),因此被权限拦截器提前结束,没有输出包含指定头部信息的响应。而一个被浏览器认为合格的预检请求响应必须包含如下的 Http 头部。

Access-Control-Allow-Origin: http://test.i.meituan.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

解决方案

方案 1:使用 Spring-Web 自带的 CorsFilter

由于 CorsFilter 是定义在 Web 容器中的过滤器(实现了 javax.servlet.Filter),因此其执行顺序先于 Servlet,而 SpringMVC 的入口是 DispatchServlet,因此该 Filter 会先于 SpringMVC 的所有拦截器执行。分析代码可知,CorsFilter 会获取单个请求对应的 Cors 配置做相应的处理。因此可以和 `` 很好的结合,不需要增加额外的代码。(勘误:CorsFilter 的构造需要 CorsConfigurationSource 实例,并且发生在 SpringMVC 配置文件解析之前,因此只能放在 Spring 的配置文件中,否则会发生找不到 bean 的异常;又由于 <mvc:cors> 的解析和 Spring 核心的解析不共享相同的 ParserContext 上下文,因此 SpringMVC 的跨域设置不能植入到 CorsFilter 中)

if (CorsUtils.isCorsRequest(request)) {
    CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
    if (corsConfiguration != null) {
        boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
        if (!isValid || CorsUtils.isPreFlightRequest(request)) {
            return;
        }
    }
}
filterChain.doFilter(request, response);

方案 2:自己实现 Interceptor

与方案 1 类似,把插入 Http 头的功能实现为 SpringMVC 的拦截器,然后在 <mvc:interceptors> 中声明为第一顺位。

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   if (request.getHeader(HttpHeaders.ORIGIN) != null) {
        response.addHeader("Access-Control-Allow-Origin", "http://test.i.meituan.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");
   }
   return true;
}

方案 3:增强自定义 Interceptor

利用 AOP 环绕增强所有自定义拦截器的 preHandle() 方法,令其跳过预检请求的拦截。

@Around(value = "cut()")
public Object processTx(ProceedingJoinPoint jp) throws Throwable {
   HttpServletRequest request = (HttpServletRequest) jp.getArgs()[0];
   if (request != null && CorsUtils.isPreFlightRequest(request)) {
       return true;
   } else {
       return jp.proceed();
   }
}
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    943 引用 • 1460 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
zhaozhizheng
没有人会关心你付出过多少努力,撑得累不累,摔得痛不痛,他们只会看你最后站在什么位置,然后羡慕或者鄙夷 北京