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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:https://blog.csdn.net/f641385712/article/details/101170214

每篇一句

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

前言

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

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的时候就完成初始化工作。

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

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券