【工作篇】了解升级 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
@EnableWebMvc
public 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);
}
}
- Xml 配置方式
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="http://domain1.com, http://domain2.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 {
@Override
protected 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
@EnableWebMvc
public 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 请求源码
@Override
protected 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 抽象类实现了该方法。
@Override
public 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 implements HttpRequestHandler, CorsConfigurationSource {
private final CorsConfiguration config;
public PreFlightHandler(CorsConfiguration config) {
this.config = config;
}
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
corsProcessor.processRequest(this.config, request, response);
}
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
return this.config;
}
}
CorsProcessor 的 processRequest 方法是 SpringMVC 支持 Cors 的具体实现,到此已经了解了 Spring 对 Cors 支持的源码实现。但是为什么升级到 Spring4.3.22RELEASE,部署到 Tomcat 6.0.48 会出现跨域问题,而部署到 Tomcat 8.048 则不会这个问题,我们继续看 ServletServerHttpResponse 类
@Override
@SuppressWarnings("resource")
public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
//如果设置了 Access-Control-Allow-Origin 响应头,则直接返回
if (responseHasCors(serverResponse)) {
logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
return true;
}
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (WebUtils.isSameOrigin(serverRequest)) {
logger.debug("Skip CORS processing: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(serverResponse);
return false;
}
else {
return true;
}
}
return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
}
前面项目中有自定义 Filter 来处理跨域问题,而设置了对应的跨域响应头。在 ServletServerHttpResponse 类 的构造方法里,会根据 servlet 版本实例化不同的 headers。
public ServletServerHttpResponse(HttpServletResponse servletResponse) {
Assert.notNull(servletResponse, "HttpServletResponse must not be null");
this.servletResponse = servletResponse;
this.headers = (servlet3Present ? new ServletResponseHttpHeaders() : new HttpHeaders());
}
ServletResponseHttpHeaders 与 HttpHeaders 的区别是?
- ServletResponseHttpHeaders 是 HttpHeaders 的子类
- ServletResponseHttpHeaders 在获取响应头时,会先从当前响应中获取,也会从由外部传入的 header Map 中获取
- 在实例化 ServletServerHttpResponse 类时,并没有传入 header ,所以在 servlet3 以下版本下,获取不到 Access-Control-Allow-Origin 响应头,没有跳过 Cors 请求处理
//ServletResponseHttpHeaders.get方法
@Override
public List<String> get(Object key) {
Assert.isInstanceOf(String.class, key, "Key must be a String-based header name");
//从当前响应中获取响应头
Collection<String> values1 = servletResponse.getHeaders((String) key);
boolean isEmpty1 = CollectionUtils.isEmpty(values1);
//再调用父类HttpHeaders.get方法获取响应头
List<String> values2 = super.get(key);
boolean isEmpty2 = CollectionUtils.isEmpty(values2);
if (isEmpty1 && isEmpty2) {
return null;
}
List<String> values = new ArrayList<String>();
if (!isEmpty1) {
values.addAll(values1);
}
if (!isEmpty2) {
values.addAll(values2);
}
return values;
}
三、总结
- 在设置 Access-Control-Allow-Origin 时,要注意验证请求域名合法问题
- 平常要注意与正式环境配置一置,在小公司很多问题都没有意识到
- 虽然这次的问题很简单,但是要多问为什么? 多研究一下,才能提升自己
相关实践代码
参考
- 跨域资源共享 CORS 详解 阮一峰
- https://developer.mozilla.org/zh-TW/docs/Web/HTTP/CORS
- http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
- https://www.ruanyifeng.com/blog/2019/09/cookie-samesite.html
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Set-Cookie/SameSite
- https://github.com/spring-projects/spring-framework/issues/13916
- https://www.cnblogs.com/wxw16/p/10674539.html
- Spring4.2 开始支持 CORS 跨域
- Tomcat
【工作篇】了解升级 Spring 版本导致的跨域问题的更多相关文章
- Tomcat8.5 升级tomcat版本导致出现异常,Base64不存在
Tomcat8.5 升级tomcat版本导致出现异常,Base64不存在 原因分析: 由于tomcat由7升级到8.5导致Base64的引用路径错误,默认引用为8.5中的jar, 解决方案: 修改引用 ...
- Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十二):解决跨域问题
什么是跨域? 同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源. 同源策略是浏览器安全的基石. 如果一个请求地址里面的协议.域名和端口号都相同,就属于同源. ...
- spring could 微服务 跨域问题(CORS )
问题发现 正常情况下,跨域是这样的:1. 微服务配置跨域+zuul不配置=有跨域问题2. 微服务配置+zuul配置=有跨域问题3. 微服务不配置+zuul不配置=有跨域问题4. 微服务不配置+zuul ...
- 升级PHP版本导致zabbix无法访问解决办法
故障现象:无法打开zabbix首页,提示缺少zabbix.conf配置文件 原因分析:升级yum安装php版本了,升级前卸载了原PHP5.4版本导致 解决办法: 重新安装zabbix yum inst ...
- Spring MVC学习总结(10)——Spring MVC使用Cors跨域
跨站 HTTP 请求(Cross-site HTTP request)是指发起请求的资源所在域不同于该请求所指向资源所在的域的 HTTP 请求.比如说,域名A(http://domaina.examp ...
- spring boot:解决cors跨域问题的两种方法(spring boot 2.3.2)
一,什么是CORS? 1,CORS(跨域资源共享)(CORS,Cross-origin resource sharing), 它是一个 W3C 标准中浏览器技术的规范, 它允许浏览器向非同一个域的服务 ...
- Java Spring boot 2.0 跨域问题
跨域 一个资源会发起一个跨域HTTP请求(Cross-site HTTP request), 当它请求的一个资源是从一个与它本身提供的第一个资源的不同的域名时 . 比如说,域名A(http://dom ...
- Nginx完美解决前后端分离端口号不同导致的跨域问题
笔者在做前后端分离系统时,出现了很多坑,比如前后端的url域名相同,但是端口号不同.例如前端页面为:http://127.0.0.1/ , 后端api根路径为 http://127.0.0.1:888 ...
- Spring @CrossOrigin 通配符 解决跨域问题
@CrossOrigin 通配符 解决跨域问题 痛点: 对很多api接口需要 开放H5 Ajax跨域请求支持 由于环境多套域名不同,而CrossOrigin 原生只支持* 或者具体域名的跨域支持 所以 ...
随机推荐
- ESP32使用SPIFFS文件系统笔记
基于ESP-IDF4.1 1 #include <stdio.h> 2 #include <string.h> 3 #include <sys/unistd.h> ...
- 解决 .net core 中 nuget 包版本冲突问题[转载]
今天在一个 asp.net core 项目中遇到了 nuget 包版本冲突的问题,错误信息如下: Version conflict detected for Microsoft.AspNet.WebA ...
- postgresql分组后获取第一条数据
-- 根据编号分组取第一条数据 select * from table t where t.no=(select max(no) from table t1 where t1.no=t.no) -- ...
- python使用笔记004-冒泡排序
冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法. 它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小.首字母从Z到A)错误就把他们交换过来.走访元素 ...
- python + flask轻量级框架
from flask import Flask,jsonify,make_response,abort,Response,request from flask_restful import Api,R ...
- UI自动化学习笔记- PO模型介绍和使用
一.PO模型 1.PO介绍:page(页面) object(对象) 在自动化中,Selenium 自动化测试中有一个名字经常被提及 PageObject (思想与面向对象的特征相同),通常PO 模型可 ...
- 给你的Mac 整个好用的命令行iTerm2 + zsh + oh-my-zsh + powerlevel10k
给你的Mac 整个好用的命令行iTerm2 + zsh + oh-my-zsh + powerlevel10k 介绍 iTerm2 是一个MacOS 下的终端模拟器,和其他的终端本质上没啥大不同.但相 ...
- ThinkPHP中使用Verify类生产验证码不显示的原因
今天在做网站部署的时候,发现登录页面的验证码显示不出来了,而且不报任何错误. 直接通过url访问该操作也不能显示. 后来在网上查找了一些解决方法. 在调用$verify = new \Think\Ve ...
- java 8新特性 并行流
使用并行流,提高cpu利用率,提高运算速度 /** * java 8并行流 * 底层运用fork join框架 */ @Test public void test(){ Instant start = ...
- windows 查看端口号,关闭端口进程
1.打开cmd,输入:netstat -ano | findstr 8080,根据端口号查找对应的PID.结果如下: 2.根据PID找进程名称,输入命令:tasklist | findstr 1789 ...