CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例
前言
通过前两篇文章做好了的铺垫和讲述,现在的你应该了解了CORS
是怎么回事以及Spring MVC
对它是如何支持的,我有理由相信你现在完全是有能力去解决CORS
跨域请求问题,而不用再是两眼一抹黑了。 正所谓好人做到底,送佛送到西,小伙伴一直最为关心Spring MVC
对CORS
的落地实操示例我还没有给出,当然还有它的处理流程原理分析,那么本文就是你最应该关注和收藏的了。
CROS跨域请求处理方式
针对CORS
跨域请求的处理,了解了基础知识后的我们知道,即使没有Spring MVC
的支持我们也是能够自行处理的,毕竟在Spring4.2
之前都是开发者自己手动向HttpServletResponse
设置请求头来解决问题的。 对于新时代的开发者,显然这种硬编码的方式就需要被淘汰el。Spring MVC
内置的支持方式有多种,可谓非常多样和灵活。下面就聊聊这些处理方式并给出示例Demo
,仅供参考。
方式一:自定义Filter/HandlerInterceptor
前面有说到,Spring
直到4.2版本才提供了对CORS
的支持,因此对于一些老项目,一般会使用自定义的Filter
/拦截器来处理:
// 自定义一个Filter来处理CORS跨域请求
@Component
public class CORSFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
// TODO:这里应该是只需要处理OPTIONS请求即可~~~
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
// response.setHeader("Access-Control-Allow-Credentials", "true");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
方式二:Nginx统一配置
配置在Nginx后,后端服务就不用再操心跨域请求问题了,这是很多公司推荐的方案。 此处我贴出一个配置供以参考,copy自这里
#
# Wide-open CORS config for nginx
#
location / {
#### 对OPTIONS请求,会设置很多的请求头,并返回204
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'POST') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
}
}
上面是自定义方式解决,不强依赖于Spring MVC
框架的支持。那么下面就是使用Spring4.2
后提供的能力来灵活解决,这当然也是生厂上主流使用的方案。
方式三:CorsFilter
Spring MVC 4.2
后内置了一个CorsFilter
专门用于处理CORS
请求问题,它所在的路径是:org.springframework.web.filter.CorsFilter
。通过配置这个Filter
使它生效便可统一控制跨域请求(URL级别控制):
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
...
// 使用javax.servlet.ServletContainerInitializer方式注册Filter
@Override
protected void registerDispatcherServlet(ServletContext servletContext) {
super.registerDispatcherServlet(servletContext);
// 注册Jar包内 内置的Filter等等
UrlBasedCorsConfigurationSource confiurationSource = new UrlBasedCorsConfigurationSource();
// 根据URL配置其对应的CORS配置 key支持的是AntPathMatcher.match()
// 说明:这里请使用LinkedHashMap,确保URL的匹配顺序(/**请放在最后一个)
Map<String, CorsConfiguration> corsConfigs = new LinkedHashMap<>();
//corsConfigs.put("*", new CorsConfiguration().applyPermitDefaultValues());
// '/**'表示匹配所有深度的路径
corsConfigs.put("/**", new CorsConfiguration().applyPermitDefaultValues());
confiurationSource.setCorsConfigurations(corsConfigs);
// /*表示所有请求都用此filter处理一下
servletContext.addFilter("corsFilter", new CorsFilter(confiurationSource))
.addMappingForUrlPatterns((EnumSet.of(DispatcherType.REQUEST)), false, "/*");
}
}
我觉得这个示例的难点反倒是注册这个Jar包内的Filter,若是SpringBoot
环境大伙都会注册,但本文示例是全注解驱动的Spring MVC
(木有web.xml
)环境。关于它的更多注册方式,可参见这里
配置好Filter后,点击发送按钮,即可正常跨域访问了。
方式四:@CrossOrigin
如果觉得使用CorsFilter
配置起来麻烦,或者你想实现精细化且更加简便的控制,那么@CrossOrigin
这个注解你值得拥有。 它使用方式极其简单,如下案例:
@CrossOrigin(origins = "http://localhost:63342", methods = {GET, POST, PUT, DELETE}, maxAge = 60L)
@RequestMapping(value = "/test/cros", method = {OPTIONS, GET})
public Object testCros() {
return "hello cros";
}
这样点击发送便能正常跨域请求了,截图如下:
难道每个Controller
都显示的写上这个注解来处理?当然不是,除了这种局部配置外,Spring MVC
还提供了下面这种全局配置的方式
方式五:WebMvcConfigurer
方式全局配置
Spring MVC
提供的这种配置方法我个人认为是最好的方式,能解决几乎所有问题。从语义配置上能表现出这是web层的东西而非其它,从使用上也非常的简单,因此我个人推荐这种方式而非Filter方式。
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/test/cros")
// -------addMapping后还可以继续配置-------
.allowedOrigins("http://localhost:63342")
.maxAge(300L);
registry.addMapping("/**").allowedOrigins("*");
}
}
等价的xml的方式表达:
<mvc:cors>
<mvc:mapping path="/test/cros" ... />
<mvc:mapping path="/**" ... />
</mvc:cors>
点击发送按钮当然也能正常work。截图如下:
本文我一共总结了5种方式来处理CORS
的跨域访问问题,任意一种方式其实都可达到目的。此时你是否有这样一个疑问:若配置了多种方式(特别是Spring MVC
内置的方式),生效的优先级顺序是怎样的呢?能够形成互补配置? 为了解答这个疑问,就应该先关注下Spring MVC
它对CORS
请求的一个处理流程以及配置初始化的过程。
Spring MVC处理CORS请求的流程
Spring MVC
处理任何一个reuqest请求都会去找到它的一个处理器Handler
,因此首当其冲就来到DispatcherServlet#getHandler()
这个方法~
getHandler()
对于Spring MVC
来说,每处理一个request请求都应该对应着一个Handler
:就是DispatcherServlet.getHandler()
方法来找到其对应的处理器:
DispatcherServlet:
// 根据HttpRequest从handlerMappings找到对应的handler
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
// 开启Spring MVC后默认情况下handlerMappings的长度是4
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
handlerMappings
它的长度默认是3,内容如下:
处理本例请求的是RequestMappingHandlerMapping
,获取处理器的方法在父类上:
AbstractHandlerMapping:
// 默认使用的是UrlBasedCorsConfigurationSource来管理跨域配置
private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
// 使用的都是本类的pathMatcher和urlPathHelper
public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
Assert.notNull(corsConfigurations, "corsConfigurations must not be null");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.setCorsConfigurations(corsConfigurations);
source.setPathMatcher(this.pathMatcher);
source.setUrlPathHelper(this.urlPathHelper);
this.corsConfigurationSource = source;
}
// @since 5.1 此方法出现较晚,但一般也不建议去设置
public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
this.corsConfigurationSource = corsConfigurationSource;
}
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// getHandlerInternal这个方法是根据URL去匹配一个Handler,当然有可能是匹配不上的,那么handler就为null
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
// 若最终还是为null,那就返回null 后续的也就不再处理了
// 它的结果是:交给下一个HandlerMapping处理,若所有的处理完后还是返回null。
// 那就noHandlerFound(processedRequest, response) --> 404
if (handler == null) {
return null;
}
...
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
...
// 若是跨域请求,这里就继续处理,也是本文讲述具有差异性的地方所在
if (CorsUtils.isCorsRequest(request)) {
// 1、全局配置:从UrlBasedCorsConfigurationSource找到一个属于这个请求的配置
// 请注意:若三种方式都没有配置,这里返回的就是null~~~
CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
// 2、从handler自己里找:若handler自己实现了CorsConfigurationSource接口,那就从自己这哪呗
// 说明:此种方式适用于一个类就是一个处理器的case。比如servlet处理器
// 所以对于@RequestMapping情况,这个值大部分情况都是null
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
// 3、把全局配置和handler配置combine组合合并
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
// 4、这个方法很重要。请看下面这个方法
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
}
// @since 4.2
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
// 若是预检请求:就new一个新的HandlerExecutionChain。
// PreFlightHandler是一个HttpRequestHandler哦~~~并且实现了接口CorsConfigurationSource
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
// 若不是预检请求,就添加一个拦截器CorsInterceptor
// 注意:这个拦截器只会作用于这个chain哦(也就是这个handler~~~)
// 能进来这里是简单请求 或者 真实请求。
else {
chain.addInterceptor(new CorsInterceptor(config));
}
return chain;
}
根据URL
成功匹配到一个Handler
后,若是跨域请求就会继续添加跨域部分的处理逻辑:
- 若是预检请求:针对此请求会直接
new
一个PreFlightHandler
作为HttpRequestHandler
处理器来处理它,而不再是交给匹配上的Handler
去处理(这点特别的重要) -PreFlightHandler#handle
方法委托给了corsProcessor
去处理跨域请求头、响应头的 - 值得注意的是:此时即使原Handler它不执行了,但匹配上的HandlerInterceptor
们仍都还是会生效执行作用在OPTIONS
方法上的 - 若是简单请求/真实请求:在原来的处理链上加一个拦截器
chain.addInterceptor(new CorsInterceptor(config))
,由这个拦截器它最终复杂来处理相关逻辑(全权委托给corsProcessor
)
核心的处理步骤就这么简单,理解起来也并不困难。因此我们还非常有必要的就是这三种配置方式是如何被初始化的呢?
CorsFilter方式初始化
要让它生效就需要我们手动把它注册进Servlet容器内,由它“拦截请求”自己来完成CorsProcessor.processRequest(corsConfiguration, request, response)
这些处理操作。所以它和后续的getHandler()
等这些处理逻辑是关系不大的。 此种方式的优雅程度上和自己实现差异并不大,因此我个人是不太推荐的~~
WebMvcConfigurer.addCorsMappings()方式初始化
这种方式是我推荐的,它的基本原理和我之前说过的WebMvcConfigurer
其它配置项差不多。它作用的地方就是下面我列出的4个HandlerMapping
初始化的时候。
WebMvcConfigurationSupport:
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
...
}
// 最终返回的是个SimpleUrlHandlerMapping 可以直接完成映射
@Bean
@Nullable
public HandlerMapping viewControllerHandlerMapping() {
ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
...
}
// 按照bean名称进行匹配处理器
@Bean
public BeanNameUrlHandlerMapping beanNameHandlerMapping() {}
// 最终也是个SimpleUrlHandlerMapping
@Bean
@Nullable
public HandlerMapping resourceHandlerMapping() {}
他们四个初始化时最终都调用了同一个方法:mapping.setCorsConfigurations(getCorsConfigurations())
设置CORS
配置,此方法是父类AbstractHandlerMapping
提供的,原理可参考CorsRegistry和CorsRegistration
。
@CrossOrigin初始化
关于此注解的初始化,在完成mapping
注册的时候就已经完成了,大致步骤如下:
AbstractHandlerMethodMapping:
// 注册一个mapping
public void registerMapping(T mapping, Object handler, Method method) {
this.mappingRegistry.register(mapping, handler, method);
}
// 内部类
class MappingRegistry {
// 记录着没一个HandlerMethod所对应的注解配置
private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
...
public void register(T mapping, Object handler, Method method) {
...
// initCorsConfiguration这里就是解析handler上面的注解喽~~~
// 此init方法只有RequestMappingHandlerMapping子类重写了~~~
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) { // 若不为null(有注解配置),就缓存起来
this.corsLookup.put(handlerMethod, corsConfig);
}
...
}
}
对于handler上次注解的解析,最终是由RequestMappingHandlerMapping
完成的:
RequestMappingHandlerMappin:
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
...
// 找到类上和方法上的注解(若都为null就返回null)
// 说明:此注解可以标注在父类、接口上
CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
... // combine合并这两个部分(若有两个的话)
// 最终执行它:兜底(防止注解上很多属性都木填)
return config.applyPermitDefaultValues();
}
它显著的特点是:和Handler
强绑定,因此在注册Mapping的时候就完成初始化工作。
综上所述可得出这三种配置方式的区别:
CorsFilter
方式:完全独立的Filter
,和其它配置并不冲突和也无关联,最终委托给CorsProcessor
来完成的addCorsMappings
方式:它的配置会作用于所有的内置配置的HandlerMapping
上,所以它就是global全局配置@CrossOrigin
方式:它和某一个具体的handler强绑定,所以它属于局部配置。
说明:方式2和方式3可以形成互补配置,有combine的效果。
为何OPTIONS
请求进入不了Controller
的Handler方法内?
这个问题是系列文章的第一篇我抛出来的,因为有一个现象是:简单请求我可以在Controller
的方法内向response手动添加请求头搞定。但是非简单请求这么做行不通了,原因是OPTIONS
请求根本进入不了方法体~
阅读完本文的上半拉,此问题的答案就显而易见了,因此我此处不再废话。倘若对此问题还没想到答案的小伙伴,欢迎你在下面给我留言我会及时解答你的。
为何给response设置响应头写在postHandle()
方法内无效?
这个问题倒是困扰了我好一会,直到我直到了Spring MVC
对它的处理过程。 问题的现象是:response
的响应头都有,但http状态码却是403,跨域失败。结果如下截图:
针对此问题作出如下解释供以参考:
- 上面有说到一句话:匹配上
handler
后,若是OPTIONS
请求的话,它最终的handler不是原handler而是一个全新的PreFlightHandler
处理器,并且并且并且chain上的拦截器们都是会生效的。 - 关键就在这里:
PreFlightHandler
执行handler处理方法最终是委托给CorsProcessor
执行的,若config == null
并且是 预检请求 ,那它就会执行:rejectRequest(serverResponse)
,这时状态码就已经设置为了403了,因此等handler方法执行完成之后再执行postHandle()
方法体,因为返回状态码已经设置好,已经无力回天了,so就出现了如此怪异现象~
有人说在
postHandle()
方法里加上这么一句,手动把响应码改成200:response.setStatus(HttpStatus.OK.value());
。 效果:能达到想要的跨域效(真实请求能继续发送)。但是我强烈不建议你这么去做,因此这样你需要加很多逻辑判断(什么时候应该设置,什么时候不应该),得不偿失。
DispatcherServlet.doOptions()方法简单分析
说明:dispatchOptionsRequest
这个参数虽然默认值是false,但在DispatcherServlet
所有的构造器里都有这么一句:setDispatchOptionsRequest(true)
。
FrameworkServlet:
/** Should we dispatch an HTTP OPTIONS request to {@link #doService}?. */
private boolean dispatchOptionsRequest = false;
@Override
protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 若dispatchOptionsRequest = true 或者是预检请求OPTIONS请求,都会processRequest
// processRequest(request, response);就是复杂的视图渲染逻辑~~~
if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
processRequest(request, response);
// 若你自己设置了allow响应头,那就不处理了。否则交给下面处理
if (response.containsHeader("Allow")) {
// Proper OPTIONS response coming from a handler - we're done.
return;
}
}
// Use response wrapper in order to always add PATCH to the allowed methods
// 开发者自己没有设置Allow这个响应头就会进这里来,最终效果是
// Allow:GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
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);
}
});
}
若CORS请求的URL不存在
,响应码404还是403?
- 无默认的servlet处理器(
DefaultServletHandler
):404(找不到对应的handler) - 有默认的servlet处理器:403(能找到handler,因为有默认的处理器兜底嘛)
Spring MVC
的这个配置用于开启默认处理器与否:
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
//configurer.enable();
//configurer.enable("default");
}
(。・v・。)