SpringBoot处理CORS问题
1.什么是CORS
Cross-origin resource sharing,缩写:CORS ,
俗称:跨域资源共享,
表现症状:Access to XMLHttpRequest at ‘http://172.21.202.120:8080/chart’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
是一种用于让网页的受限资源能够被其他域名的页面访问的一种机制。说白了CORS就是一种桥接机制,用作网页间的受限资源访问。网页资源为啥会受限呢?是因为浏览器的同源策略,众所周知http是无状态协议,是不安全的。浏览器为了保证安全性,阻隔恶意文档,减少可能被攻击的媒介。于是便设计了同源策略,用于限制一个源的文档或者它加载的脚本如何能与另一个源的资源进行交互。 例如,XMLHttpRequest和Fetch API遵循同源策略。 这意味着使用这些API的Web应用程序只能从加载应用程序的同一个域请求HTTP资源,除非响应报文包含了正确CORS响应头。就像这样:
一个源(Origin)包含了协议,地址,端口源组 就像这样一个源http://eqxiu.chart.com/dir/page.html
这里举一些与这个源,同源或非同源的例子如下:
URL | 结果 | 原因 |
---|---|---|
http://eqxiu.chart.com/dir2/other.html | 同源 | 只有路径不同 |
http://eqxiu.chart.com/dir/inner/another.html | 同源 | 只有路径不同 |
https://eqxiu.chart.com/secure.html | 不同源 | 协议不同 |
http://eqxiu.chart.com:81/dir/etc.html | 不同源 | 端口不同,默认端口是80 |
http://other.company.com/dir/other.html | 不同源 | 主机不同 |
问题和原因都找到了,现在就该回到具体业务场景来解决问题了,思路基本就是在response头中加入CORS允许的控制。参考Spring的官方文档有如下两种解决方案:https://spring.io/guides/gs/rest-service-cors/#global-cors-configuration
2.SpringBoot处理CORS的方式
2.1 @CrossOrigin注解的方式
这种方式粒度更小:对需要跨域请求的接口@RequestMapping前加上一个@CrossOrigin解并配置参数即可,就像这样@CrossOrigin(origins = “http://xx-domain.com”, allowCredentials=“true”)。
2.2 全局配置类的方式
这种方式就是全局配置跨域了,新增一个配置类,所有接口都接受跨域请求:
@Configuration
public class GlobalCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
//重写父类提供的跨域请求处理的接口
public void addCorsMappings(CorsRegistry registry) {
//添加映射路径
registry.addMapping("/**")
//放行哪些原始域
.allowedOrigins("*")
//是否发送Cookie信息
.allowCredentials(true)
//放行哪些原始域(请求方式)
.allowedMethods("GET","POST", "PUT", "DELETE")
//放行哪些原始域(头部信息)
.allowedHeaders("*")
//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)
.exposedHeaders("Header1", "Header2");
}
};
}
}
3.自定义拦截器的使用导致CORS配置失效
这是解决跨域问题遇到在一个小坑,最后分析出来是因为拦截器顺序导致跨域失效,场景是我写了一个自定义拦截器用来拦截请求,并实现校验前端传过来的header合法性的功能。在接口的开发中,通过Postman测试没有任何问题,但是在联调的时候,出现了问题,对于简单的GET请求可以正常执行,没有任何问题,但是当执行一个POST请求,且附带header时,浏览器却报错:不允许跨域请求。
由于这个POST请求使用了自定义header,我们可以知道这个请求属于需要预检(Preflight Request)的请求,浏览器会首先发送一个预检的请求到服务器,问题就是出现在这个预检请求上。由于预检请求是浏览器自动发送的,没有包含自定义的header,所以在我实现的拦截器中不能通过。但是这里有个问题,那就是如果是header校验不通过,应该返回的是自定义的校验失败信息而不是直接跨域请求失败,到底出了什么问题?经过一段时间的debug和google得出结论是和拦截器的处理顺序,排查过程再现如下:
DispatchServlet.doDispatch()方法是Spring的核心入口方法,权限拦截器的实现主要是在preHandle()方法中,Spring MVC对跨域请求处理的过程在handle()中,实际是corsProcessor对象(类型为:DefaultCorsProcessor)的processRequest方法来处理。而CorsInterceptor唯一任务就是调用corsProcessor.processRequest来为request注入CORS头.但是拦截器的特性是:任何一个拦截器不通过直接就跳过后续的处理过程。预检请求中没有包含自定义的header,所以先被自定义拦截器拦截,直接返回了错误信息,没有经过跨域处理,响应中没有跨域信息,因此被浏览直接拦截响应,最终得到了错误信息:不允许跨域请求。
解决方案就是利用AOP来增强所有自定义拦截器的preHandle()方法
就像这样:
@Component
@Aspect
public class CustomerInterceptorAOP {
@Pointcut("execution(public * com.eqxiu.chart.Interceptor.*.preHandle(..))")
public void poinCut() {
}
@Around(value = "poinCut()")
public Object processTx(ProceedingJoinPoint proceedJoinPoint) throws Throwable {
HttpServletRequest request = (HttpServletRequest) proceedJoinPoint.getArgs()[0];
if (request != null && CorsUtils.isPreFlightRequest(request)) {
return true;
} else {
return proceedJoinPoint.proceed();
}
}
}
4.One More Thing
CORS问题不单单是前端或者后端的问题,需要前后端人员一起协调处理。在前端发请求的时候需要把XMLHttpRequest请求时,需要把withCredentials 参数置为ture。