2021-07-19

【工作篇】了解升级 Spring 版本导致的跨域问题

一、背景

最近需要统一升级 Spring 的版本,避免 common 包和各个项目间的 Spring 版本冲突问题。这次升级主要是从 Spring 4.1.9.RELEASE 升级到 Spring 4.3.22RELEASE。

预备知识点

  • OPTIONS 请求 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONS
  • CORS 跨域请求
  • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
  • https://www.ruanyifeng.com/blog/2016/04/cors.html

升级前相关环境

项目采用的方式是通过实现过滤器 Filter,在 Response 返回头文件添加跨域资源共享(CORS) 相关的参数。采用打 war 包部署到 Tomcat6.0.48,但是本地开发配置的 tomcat 版本是 Tomcat8.0.48(这里一般要与服务器环境一致,不然有不可预知问题出现)。

public class CrossFilter extends OncePerRequestFilter {	@Override	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)			throws ServletException, IOException {		response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");		response.setHeader("Access-Control-Max-Age", "3600");		response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, CMALL_TOKEN"); //这里自定义的请求头不规范,应该使用"-",CMALL-TOKEN,不然需要配置nignx识别		response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie		String origin = request.getHeader("Origin");		response.setHeader("Access-Control-Allow-Origin", origin); //注意:这里以前并没有限制合法的 域名		//注意:这里如果是预检请求,还是会执行下一个Filter,最好是直接返回响应前端		chain.doFilter(request, response);	}}

二、排查问题

在本地开发环境升级了 Spring 版本为后 Spring 4.3.22RELEASE 后,没有修改 CorsFilter 相关的参数,运行测试没有跨域问题,其它功能正常。 然后部署到测试环境,发现了跨域问题。

通过排查,发现本地的 Tomcat 版本是 Tomcat8.0.48,而测试环境的版本是 Tomcat6.0.48,大意了,平常开发环境也没有注意规范,要与线上,测试等环境保持一致。本地重新配置 Tomcat6.0.48 后重现了跨域问题。

2.1、初步分析

开始排查具体的失败问题,发现

1、Spring4.3.22RELEASE tomcat 6.048 会出现跨域问题

2、Spring 4.1.9RELEASE (Tomcat6.0.48、Tomcat 8.0.48 ) 不会出现跨域问题

3、Spring4.3.22RELEASE (Tomcat8.048) 不会出现跨域问题

从而得出以下疑问?

1、Spring 4.1.9RELEASE 到 Spring4.3.22RELEASE 版本,针对 CORS,有什么新特性发布?

2、Tomcat6.0.48、Tomcat 8.0.48 有什么区别?

2.1.1、首先查看 Spring 版本的差异

通过查看 SpringMVC 官方文档,从 4.2.0 版本开始,SpringMVC 开始支持 CORS 跨域解决方案,主要表现是通过简单的配置,就可以支持 CORS

  • https://docs.spring.io/spring-framework/docs/4.2.0.RELEASE/spring-framework-reference/html/cors.html
  • https://github.com/spring-projects/spring-framework/issues/13916

主要可以通过以下方式配置跨域支持

  • 1、通过注解 @CrossOrigin 为单独的请求配置跨域
@RestController@RequestMapping("/account")public class AccountController {@CrossOrigin@RequestMapping("/{id}")public Account retrieve(@PathVariable Long id) {	// ...}@RequestMapping(method = RequestMethod.DELETE, path = "/{id}")public void remove(@PathVariable Long id) {	// ...}}
  • 2、全局配置方式

    • Java Config  配置方式
@Configuration@EnableWebMvcpublic class WebConfig extends WebMvcConfigurerAdapter {	@Override	public void addCorsMappings(CorsRegistry registry) {		registry.addMapping("/api/**")			.allowedOrigins("http://domain2.com")			.allowedMethods("PUT", "DELETE")			.allowedHeaders("header1", "header2", "header3")			.exposedHeaders("header1", "header2")			.allowCredentials(false).maxAge(3600);	}}
<mvc:cors>	<mvc:mapping path="/api/**"		allowed-origins="http://domain1.com,		allowed-methods="GET, PUT"		allowed-headers="header1, header2, header3"		exposed-headers="header1, header2" allow-credentials="false"		max-age="123" />	<mvc:mapping path="/resources/**"		allowed-origins="http://domain1.com" /></mvc:cors>
2.1.2、Tomcat 版本的关键区别

查看 Tomcat 版本的发布信息:

  • https://archive.apache.org/dist/tomcat/tomcat-6/v6.0.48/RELEASE-NOTES
  • https://archive.apache.org/dist/tomcat/tomcat-8/v8.0.48/RELEASE-NOTES

得出对于这次跨域问题,可能有影响的区别是:

  • Tomcat 6.0 支持的 Servlet 版本为 2.5
  • Tomcat 8.0 支持的 Servlet 版本为 3.1

2.2、得出解决方案

对于上面的查找资料的过程,其实已经可以得出解决方案了(升级到 Spring4.3.22RELEASE):

因为我们使用的是自实现 Filter 过滤器的方式来处理跨域问题的,是不涉及框架问题才对,这里主要是我们没有对预检请求进行拦截并响应告知前端通过跨域请求。

  • 方法一、为了不怎么改动代码,我们还是采用在原来的过滤器中处理预检请求
public class CorsFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {	response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");	response.setHeader("Access-Control-Max-Age", "3600");	response.addHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, CMALL-TOKEN");	response.setHeader("Access-Control-Allow-Credentials", "true"); // cookie	response.setHeader("Access-Control-Allow-Origin", "http://localhost:63342");	String origin = request.getHeader("Origin");//响应预检请求//不让过滤器执行下去,Spring默认配置的cors跨域处理器就没法处理处理OPTIONS请求	if (origin != null &&			HttpMethod.OPTIONS.matches(request.getMethod()) &&			request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null) {		response.setStatus(HttpServletResponse.SC_OK);		return;	}	filterChain.doFilter(request, response);}}
  • 方法二、抛弃原先写的过滤器,使用 Spring 提供的方案
@Configuration@EnableWebMvcpublic class CorsConfig extends WebMvcConfigurerAdapter {	@Override	public void addCorsMappings(CorsRegistry registry) {		registry.addMapping("/**")				.allowedOrigins("http://localhost:63342")				.allowedMethods("POST", "GET", "OPTIONS", "DELETE", "PUT")				.allowedHeaders("Origin", "X-Requested-With", "Content-Type", "Accept")				.exposedHeaders("CMALL-TOKEN")				.allowCredentials(true)				.maxAge(3600);	}}

2.3、深入源码分析

虽然解决了这个跨域问题,但是还是要看看没有修改代码前为什么升级到 Spring4.3.22RELEASE,部署到 Tomcat 6.0.48 会出现跨域问题,而部署到 Tomcat 8.048 则不会。

2.3.1、回顾一下 SpringMVC 的执行过程

  • 用户发送请求经过 Filter 过滤器,Spring 拦截器,到达前端处理器 DispatchServlet
  • DispatcherServlet 收到请求调用 HandlerMapping(处理器映射器)
  • HandlerMapping 找到具体的处理器(Controller) 和 处理器拦截器(HandlerInterceptor)组成处理器执行链对象
  • DispatcherServlet 通过处理器(Controller)找到对应的处理器适配器(HandlerAdapter)
  • 处理器适配器(HandlerAdapter)执行具体的处理器(Controller)
  • Controller 执行完成返回 ModelAndView 对象。
  • DispatcherServlet 将 ModelAndView 传给 ViewReslover(视图解析器)。
  • ViewReslover 解析后返回具体 View(视图)。
  • DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中)。
  • DispatcherServlet 响应用户。
2.3.2、Spring 是如何提供 CORS 支持的?

SpringMVC 的入口文件 DispatcherServlet,默认情况下 DispatcherServlet 继承自 FrameworkServlet,FrameworkServlet 处理了所有的 http 请求,调用 processRequest() 方法。

SpringMVC 处理 Option 请求源码

@Overrideprotected void doOptions(HttpServletRequest request, HttpServletResponse response)		throws ServletException, IOException {	//dispatchOptionsRequest 是否开启对options请求的处理,默认值false	//CorsUtils.isPreFlightRequest(request) 判断是否是预检请求	if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {		//处理 OPTIONS 请求		processRequest(request, response);		//包含 Allow响应头部,则请求已被正常处理,直接返回		if (response.containsHeader("Allow")) {			// Proper OPTIONS response coming from a handler - we're done.			return;		}	}	//调用父类的doOptions()方法,用于设置 Allow 响应头部	// Use response wrapper for Servlet 2.5 compatibility where	// the getHeader() method does not exist	super.doOptions(request, new HttpServletResponseWrapper(response) {		@Override		public void setHeader(String name, String value) {			if ("Allow".equals(name)) {				value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();			}			super.setHeader(name, value);		}	});}

在执行 processRequest 方法时的执行链是: FrameworkServlet.processRequest()->DispatcherServlet.doService()->DispatcherServlet.doDispatch()。

		...try {ModelAndView mv = null;Exception dispatchException = null;try {	processedRequest = checkMultipart(request);	multipartRequestParsed = (processedRequest != request);	// Determine handler for the current request.	// 获取HandlerMapping(处理器映射器)	mappedHandler = getHandler(processedRequest);	if (mappedHandler == null || mappedHandler.getHandler() == null) {		noHandlerFound(processedRequest, response);		return;	}	// Determine handler adapter for the current request.	//处理器适配器(HandlerAdapter)	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;		}	}	//执行拦截器的前置方法	if (!mappedHandler.applyPreHandle(processedRequest, response)) {		return;	}	// Actually invoke the handler.	//执行具体的控制器(Controller)	mv = ha.handle(processedRequest, response, mappedHandler.getHandler());	if (asyncManager.isConcurrentHandlingStarted()) {		return;	}	applyDefaultViewName(processedRequest, mv);	mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {	dispatchException = ex;}catch (Throwable err) {	// As of 4.3, we're processing Errors thrown from handler methods as well,	// making them available for @ExceptionHandler methods and other scenarios.	dispatchException = new NestedServletException("Handler dispatch failed", err);}			...

继续查看 CORS 的实现原理,getHandler 方法源码

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

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

@Overridepublic final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {	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);	}	//获取处理器执行链	HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);	//判断是否是跨域请求	if (CorsUtils.isCorsRequest(request)) {		//获取 cors 配置		CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);		CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);		CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);		executionChain = getCorsHandlerExecutionChain(request, executionChain, config);	}	return executionChain;}

如果是预检请求,则使用在 AbstractHandlerMapping 定义的内部类 PreFlightHandler 处理器处理预检请求

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;}

而 PreFlightHandler 又委托给 CorsProcessor 处理

private CorsProcessor corsProcessor = new DefaultCorsProcessor();private class PreFlightHandler i......

原文转载:http://www.shaoqun.com/a/888713.html

跨境电商:https://www.ikjzd.com/

moss:https://www.ikjzd.com/w/1653

taofenba:https://www.ikjzd.com/w/1725

parser:https://www.ikjzd.com/w/680


一、背景最近需要统一升级Spring的版本,避免common包和各个项目间的Spring版本冲突问题。这次升级主要是从Spring4.1.9.RELEASE升级到Spring4.3.22RELEASE。预备知识点OPTIONS请求https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Methods/OPTIONSCORS跨域请求https://deve
世界之窗到罗湖口岸要多久?罗湖过关多少钱?_世界之窗旅游攻略:http://www.30bags.com/a/428024.html
世界之窗的阿尔卑斯冰雪世界雪场不开放了吗?:http://www.30bags.com/a/397866.html
世界之窗的阿尔卑斯冰雪世界雪场不开放了吗?_世界之窗旅游攻略:http://www.30bags.com/a/428060.html
世界之窗附近各景点的门票价格?:http://www.30bags.com/a/398015.html
父子三根一起会坏掉的好痛 他慢慢的每一下都撞到最里面:http://lady.shaoqun.com/a/248414.html
老头粗大强行戳进 老头的下面又粗又大:http://lady.shaoqun.com/a/247915.html
口述:小姨子与姐夫之间那些不要脸的事儿(上)(4/4):http://lady.shaoqun.com/m/a/82892.html
公捡筷子时我故意把腿张开 他的手在课桌下揉捏着她:http://www.30bags.com/m/a/249838.html
南部满福水世界门票2021年7月价格:http://www.30bags.com/a/513891.html
老婆和租客在厕所做不可说的事,老公要离婚!妻子:修理水管:http://lady.shaoqun.com/a/426297.html
印度公开拍卖女孩的"初夜",便宜免费玩,可耻:http://lady.shaoqun.com/a/426298.html
女床这个小习惯不好说,但是有七大健康好处!:http://lady.shaoqun.com/a/426299.html

No comments:

Post a Comment