CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】

每篇一句

架构是慢慢演进出来的,不是设计出来的。架构没有最好,只有最合适

前言

通过前两篇文章做好了的铺垫和讲述,现在的你应该了解了CORS是怎么回事以及Spring MVC对它是如何支持的,我有理由相信你现在完全是有能力去解决CORS跨域请求问题,而不用再是两眼一抹黑了。
正所谓好人做到底,送佛送到西,小伙伴一直最为关心Spring MVCCORS的落地实操示例我还没有给出,当然还有它的处理流程原理分析,那么本文就是你最应该关注和收藏的了。

CORS跨域请求处理方式

针对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/cors", method = {OPTIONS, GET})
public Object testCors() {
    return "hello cors";
}

这样点击发送便能正常跨域请求了,截图如下:
在这里插入图片描述
难道每个Controller都显示的写上这个注解来处理?当然不是,除了这种局部配置外,Spring MVC还提供了下面这种全局配置的方式

方式五:WebMvcConfigurer方式全局配置

Spring MVC提供的这种配置方法我个人认为是最好的方式,能解决几乎所有问题。从语义配置上能表现出这是web层的东西而非其它,从使用上也非常的简单,因此我个人推荐这种方式而非Filter方式。

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/test/cors")
        		// -------addMapping后还可以继续配置-------
        		.allowedOrigins("http://localhost:63342")
        		.maxAge(300L);
        registry.addMapping("/**").allowedOrigins("*");
    }
}

等价的xml的方式表达:

<mvc:cors>
	<mvc:mapping path="/test/cors" ... />
    <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的时候就完成初始化工作。

综上所述可得出这三种配置方式的区别:

  1. CorsFilter方式:完全独立的Filter,和其它配置并不冲突和也无关联,最终委托给CorsProcessor来完成的
  2. addCorsMappings方式:它的配置会作用于所有的内置配置的HandlerMapping上,所以它就是global全局配置
  3. @CrossOrigin方式:它和某一个具体的handler强绑定,所以它属于局部配置

说明:方式2和方式3可以形成互补配置,有combine的效果。



为何OPTIONS请求进入不了Controller的Handler方法内?

这个问题是系列文章的第一篇我抛出来的,因为有一个现象是:简单请求我可以在Controller的方法内向response手动添加请求头搞定。但是非简单请求这么做行不通了,原因是OPTIONS请求根本进入不了方法体~

阅读完本文的上半拉,此问题的答案就显而易见了,因此我此处不再废话。倘若对此问题还没想到答案的小伙伴,欢迎你在下面给我留言我会及时解答你的。

为何给response设置响应头写在postHandle()方法内无效?

这个问题倒是困扰了我好一会,直到我直到了Spring MVC对它的处理过程。
问题的现象是:response的响应头都有,但http状态码却是403,跨域失败。结果如下截图:
在这里插入图片描述
针对此问题作出如下解释供以参考:

  1. 上面有说到一句话:匹配上handler后,若是OPTIONS请求的话,它最终的handler不是原handler而是一个全新的PreFlightHandler处理器,并且并且并且chain上的拦截器们都是会生效的
  2. 关键就在这里: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");
    }

相关阅读

CORS跨域资源共享(一):模拟跨域请求以及结果分析,理解同源策略【享学Spring MVC】
CORS跨域资源共享(二):详解Spring MVC对CORS支持的相关类和API【享学Spring MVC】
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例,原理分析【享学Spring MVC】

总结

@CrossOrigin关注方法,CorsFilter等其它方式更关注URL
CORS系列通过三篇文章层层递进的讲述了CORS跨域请求访问自己如何处理等相关议题,在前后端分离开发模式的今天我觉得后端程序员有必要掌握这块内容,因此特撰文分享给大家,我觉得应该是能对很多人有较大帮助的,所你还有别的想法,欢迎你给我留言~

发布了274 篇原创文章 · 获赞 431 · 访问量 36万+

猜你喜欢

转载自blog.csdn.net/f641385712/article/details/101170214