CORS跨域资源共享(二):详解Spring MVC对CORS支持的相关类和API【享学Spring MVC】

  • 2019 年 10 月 7 日
  • 筆記

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

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

每篇一句

重构一时爽,一直重构一直爽。但出了问题火葬场

前言

上篇文章通过我模拟的跨域请求实例和结果分析,相信小伙伴们都已经80%的掌握了CORS到底是怎么一回事以及如何使用它。由于Java语言中的web框架几乎都是使用的Spring MVC,因此本文将聚焦于Spring MVCCORS的支持,深度分析下它对CORS支持的相关API,这也方便下一章节的灵活使用以及流程原理分析。

Spring MVC与CORS

Spring MVC一直到4.2版本“才”开始内置对CORS支持,至于为何到这个版本Spring官方才对此提供支持,我这里需要结合时间轴来给大家解释一下。 上文我有说到了CORS它属于W3C的标准。我们知道任何一个规范的形成都是非常漫长的。W3C对web标准的制定分为如下7个阶段(从上到下有序):

  1. WD(Working Draft 工作草案):不稳定也不完整
  2. CR(Candidate Recommendation 候选推荐标准):所有的已知issues都被解决了
  3. PR(Proposed Recommendation 提案推荐标准):在浏览器做各种测试,此部分不会再有实质性的改动
  4. PER(Proposed Edited Recommendation 已修订的提案推荐标准):
  5. REC(Recommendation 推荐标准,通常称之为 standard,即事实标准):几乎不会再变动任何东西
  6. RET(Retired 退役的):最后这两个是建立在REC基础上变来,成熟的技术一般都不会有后面这两个
  7. NOTE(Group Note 工作组说明):

关于这7步,从这里 可以看倒CORS的WD从2009-03-17开始,2014-01-16进入的REC阶段,可谓正式毕业。而Spring4.2是在2015-06发布给与的全面支持,从时间轴上看Spring的响应速度还是把握得不错的(毕竟CORS经历过一段时间市场的考验Spring才敢全面纳入进来支持嘛~)

Tips:在Spring4.2之前,官方没有提供内置的支持,所以那时都是自己使用Filter/拦截器来处理。它的唯一缺点就是可能没那么灵活和优雅,后续官方提供标注支持后能力更强更为灵活了(底层原理都一样)



Spring MVC中CORS相关类及API说明

所有涉及到和CORS相关的类、注解、代码片段都是Spring4.2后才有的,请保持一定的版本意识。

从截图里可以看出spring-web包提供的专门用于处理CORS的相关的类,下面有必要进行逐个分析

CorsConfiguration

它代表一个cors配置,记录着各种配置项。它还提供了检查给定请求的实际来源、http方法和头的方法供以调用。用人话说:它就是具体封装跨域配置信息的pojo。 默认情况下新创建CorsConfiguration它是不允许任何跨域请求的,需要你手动去配置,或者调用applyPermitDefaultValues()开启GET、POST、Head的支持~

几乎所有场景,创建完CorsConfiguration最后都调用了applyPermitDefaultValues()方法。也就是说你不干预的情况下,一个CorsConfiguration配置一般都是支持GET、POST、Head

// @since 4.2  public class CorsConfiguration {    	// public的通配符:代表所有的源、方法、headers...  	// 若你需要使用通配符,可以使用此静态常量  	public static final String ALL = "*";    	private static final List<HttpMethod> DEFAULT_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.GET, HttpMethod.HEAD));  	// 默认许可所有方法  	private static final List<String> DEFAULT_PERMIT_ALL = Collections.unmodifiableList(Arrays.asList(ALL));  	// 默认许可这三个方法  	private static final List<String> DEFAULT_PERMIT_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));    	// ==========把这些属性对应上文讲述的响应头们对应,和W3C标注都是对应上的=========  	@Nullable  	private List<String> allowedOrigins;  	@Nullable  	private List<String> allowedMethods;  	@Nullable  	private List<HttpMethod> resolvedMethods = DEFAULT_METHODS;  	@Nullable  	private List<String> allowedHeaders;  	@Nullable  	private List<String> allowedHeaders;  	@Nullable  	private List<String> exposedHeaders;  	@Nullable  	private Boolean allowCredentials;  	@Nullable  	private Long maxAge;    	... // 省略所有构造函数以及所有的get/set方法      	// 使用此方法将初始化模型翻转为以允许get、head和post请求的所有跨源请求的打开默认值开始  	// 注意:此方法不会覆盖前面set进去的值,所以建议此方法可以作为兜底调用。实际上Spring内部也是用它兜底的  	public CorsConfiguration applyPermitDefaultValues() {  		if (this.allowedOrigins == null) {  			this.allowedOrigins = DEFAULT_PERMIT_ALL;  		}  		if (this.allowedMethods == null) {  			this.allowedMethods = DEFAULT_PERMIT_METHODS;  			this.resolvedMethods = DEFAULT_PERMIT_METHODS.stream().map(HttpMethod::resolve).collect(Collectors.toList());  		}  		if (this.allowedHeaders == null) {  			this.allowedHeaders = DEFAULT_PERMIT_ALL;  		}  		if (this.maxAge == null) {  			this.maxAge = 1800L;  		}  		return this;  	}    	public CorsConfiguration combine(@Nullable CorsConfiguration other) { ... }    	// 根据配置的允许来源检查请求的来源  	// 返回值并不是bool值,而是字符串--> 返回可用的origin。若是null表示请求的origin不被支持  	@Nullable  	public String checkOrigin(@Nullable String requestOrigin) { ... }  	// 检查预检请求的Access-Control-Request-Method这个请求头  	public List<HttpMethod> checkHttpMethod(@Nullable HttpMethod requestMethod) { ... }  	// 检查预检请求的Access-Control-Request-Headers  	@Nullable  	public List<String> checkHeaders(@Nullable List<String> requestHeaders) {  }

这个POJO的配置,是servlet传统web以及reactive web所共用的,它提供有校验的基本方法。它的属性、校验原则和W3C的CORS标准所对应。

CorsConfigurationSource

它表示一个源,该接口主要是为请求提供一个CorsConfiguration

public interface CorsConfigurationSource {    	// 找到此request的一个CORS配置  	@Nullable  	CorsConfiguration getCorsConfiguration(HttpServletRequest request);  }

此接口方法的调用处有三个地方:

  • AbstractHandlerMapping.getHandler()/getCorsConfiguration()
  • CorsFilter.doFilterInternal()
  • HandlerMappingIntrospector.getCorsConfiguration()

因为它可以根据request返回一个CORS配置。可以把这个接口理解为:存储request与跨域配置信息的容器。它的继承树如下:

首先需要说的便是cros包下的UrlBasedCorsConfigurationSource

UrlBasedCorsConfigurationSource

它位于org.springframework.web.cors包:它里面存储着path patternsCorsConfiguration的键值对。

// @since 4.2  public class UrlBasedCorsConfigurationSource implements CorsConfigurationSource {    	// 请务必注意:这里使用的是LinkedHashMap  	private final Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<>();  	private PathMatcher pathMatcher = new AntPathMatcher();  	private UrlPathHelper urlPathHelper = new UrlPathHelper();    	... // 生路所有的get/set方法    	// 这里的path匹配用到的是AntPathMatcher.match(),默认是按照ant风格进行匹配的  	@Override  	@Nullable  	public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {  		String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);  		for (Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {  			if (this.pathMatcher.match(entry.getKey(), lookupPath)) {  				return entry.getValue();  			}  		}  		return null;  	}  }

本类它是作为AbstractHandlerMappingRequestMappingHandlerMapping)的默认跨域资源配置的管理类

HandlerMappingIntrospector

HandlerMapping内省器。它是一个帮助类用于从HandlerMapping里获取信息,这些信息用于服务特定的请求。@EnableWebMvc默认会把它放进容器里,开发者可以@Autowired拿来使用(框架内部木有使用)

这个类比较重要,Spring Cloud Netflix Zuul巧用它实现了一些功能~

// @since 4.3.1  public class HandlerMappingIntrospector implements CorsConfigurationSource, ApplicationContextAware, InitializingBean {  	@Nullable  	private ApplicationContext applicationContext;  	@Nullable  	private List<HandlerMapping> handlerMappings;    	... // 生路一些构造函数、set方法    	// 1、Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, HandlerMapping.class, true, false)  	// 2、如果第一步获取到了Beans,sort()排序一下  	// 3、若没找到,回退到`DispatcherServlet.properties`这个配置文件里去找  	@Override  	public void afterPropertiesSet() {  		if (this.handlerMappings == null) {  			Assert.notNull(this.applicationContext, "No ApplicationContext");  			this.handlerMappings = initHandlerMappings(this.applicationContext);  		}  	}    	// 从这些HandlerMapping找到MatchableHandlerMapping  	// 若一个都木有,此方法抛出异常  	@Nullable  	public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { ... }      	@Override  	@Nullable  	public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {  		Assert.notNull(this.handlerMappings, "Handler mappings not initialized");  		HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);  		for (HandlerMapping handlerMapping : this.handlerMappings) {  			HandlerExecutionChain handler = null;  			try {  				handler = handlerMapping.getHandler(wrapper);  			} catch (Exception ex) {  				// Ignore  			}  			if (handler == null) {  				continue;  			}    			// 拿到作用在此Handler上的所有的拦截器们:HandlerInterceptor  			// 若有拦截器实现了CorsConfigurationSource接口,那就返回此拦截器上的CORS配置源  			if (handler.getInterceptors() != null) {  				for (HandlerInterceptor interceptor : handler.getInterceptors()) {  					if (interceptor instanceof CorsConfigurationSource) {  						return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper);  					}  				}  			}  			// 若这个Handle本身(注意:并不是所有的handler都是一个方法,也可能是个类,所以也有可能是会实现接口的)  			// 就是个CorsConfigurationSource 那就以它的为准  			if (handler.getHandler() instanceof CorsConfigurationSource) {  				return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper);  			}  		}  		return null;  	}  }

这个自省器最重要的功能就是初始化的时候把所有的HandlerMapping都拿到了。这个处理逻辑和DispatcherServlet.initHandlerMappings是一样的,为何不提取成公用的呢???

它另外一个功能便是获取HttpServletRequest对应的CORS配置信息:

  1. 从作用在此Handler的拦截器HandlerInterceptor上获取
  2. 若拦截器里木有,那就从Handler本身获取(若实现了CorsConfigurationSource接口)
  3. 都没有就返回null
CorsInterceptor

Cors拦截器。它最终会被放到处理器链HandlerExecutionChain里,用于拦截处理(最后一个拦截)。

	private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource {  		@Nullable  		private final CorsConfiguration config;    		//拦截操作 最终是委托给了`CorsProcessor`,也就是DefaultCorsProcessor去完成处理的  		@Override  		public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  			return corsProcessor.processRequest(this.config, request, response);  		}  		@Override  		@Nullable  		public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {  			return this.config;  		}  	}

该拦截器是AbstractHandlerMapping的私有内部类,它会在每次getHandler()的时候放进去专门作用域当前跨域的请求,具体的流程在下个章节里有讲述。

PreFlightHandler

这个和上面的CorsInterceptor互斥,它最终也是委托给corsProcessor来处理请求,只是它是专门用于处理预检请求的。详见CORS请求处理流程部分。

CorsProcessor(重要)

它便是CORS真正处理器:用于接收请求和一个配置,然后更新Response:比如接受/拒绝

public interface CorsProcessor {    	// 根据所给的`CorsConfiguration`来处理请求  	boolean processRequest(@Nullable CorsConfiguration configuration, HttpServletRequest request, HttpServletResponse response) throws IOException;  }

它的唯一实现类是DefaultCorsProcessor

DefaultCorsProcessor

它遵循的是W3C标准实现的。Spring MVC中对CORS规则的校验,都是通过委托给 DefaultCorsProcessor实现的

// @since 4.2  public class DefaultCorsProcessor implements CorsProcessor {    	@Override  	@SuppressWarnings("resource")  	public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {    		// 若不是跨域请求,不处理  		// 这个判断极其简单:请求中是否有Origin请求头。有这个头就是跨域请求  		if (!CorsUtils.isCorsRequest(request)) {  			return true;  		}    		// response.getHeaders().getAccessControlAllowOrigin() != null  		// 若响应头里已经设置好了Access-Control-Allow-Origin这个响应头,此处理器也不管了  		ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);  		if (responseHasCors(serverResponse)) {  			logger.trace("Skip: response already contains "Access-Control-Allow-Origin"");  			return true;  		}    		// 即使你有Origin请求头,但是是同源的请求,那也不处理  		ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);  		if (WebUtils.isSameOrigin(serverRequest)) {  			logger.trace("Skip: request is from same origin");  			return true;  		}    		// 是否是预检请求,判断标准如下:  		// 是跨域请求 && 是`OPTIONS`请求 && 有Access-Control-Request-Method这个请求头  		boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);    		// 若config == null,分两种case:  		// 是预检请求but木有给config,那就拒绝:给出状态码403  		//   response.setStatusCode(HttpStatus.FORBIDDEN)  		//   response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));  		if (config == null) {  			if (preFlightRequest) {  				rejectRequest(serverResponse);  				return false; // 告诉后面的处理器不用再处理了  			} else { // 虽然没给config,但不是预检请求(是真是请求,返回true)  				return true;  			}  		}    		// 真正的跨域处理逻辑~~~~  		// 它的处理逻辑比较简单,立即了W3C规范理解它起来非常简单,本文略  		// checkOrigin/checkMethods/checkHeaders等等方法最终都是委托给CorsConfiguration去做的  		return handleInternal(serverRequest, serverResponse, config, preFlightRequest);  	}  	...  }

使用框架来处理跨域的好处便是:兼容性很强且灵活。它的处理过程如下:

  1. 若不是跨域请求,不处理(注意是return true后面拦截器还得执行呢)。若是跨域请求继续处理。(是否是跨域请求就看请求头是否有Origin这个头)
  2. 判断response是否有Access-Control-Allow-Origin这个响应头,若有说明已经被处理过,那本处理器就不再处理了
  3. 判断是否是同源:即使有Origin请求头,但若是同源的也不处理
  4. 是否配置了CORS规则,若没有配置: 1. 若是预检请求,直接决绝403,return false 2. 若不是预检请求,则本处理器不处理
  5. 正常处理CROS请求,大致是如下步骤: 1. 判断 origin 是否合法 2. 判断 method 是否合法 3. 判断 header是否合法 4. 若其中有一项不合法,直接决绝掉403并return false。都合法的话:就在response设置上一些头信息~~~

CorsFilter

Spring4.2之前一般自己去实现一个这样的Filter来处理,4.2之后框架提供了内置支持。

Reactive的叫org.springframework.web.cors.reactive.CorsWebFilter

// @since 4.2  public class CorsFilter extends OncePerRequestFilter {    	private final CorsConfigurationSource configSource;  	// 默认使用的DefaultCorsProcessor,当然你也可以自己指定  	private CorsProcessor processor = new DefaultCorsProcessor();    	@Override  	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {  		// 只处理跨域请求  		if (CorsUtils.isCorsRequest(request)) {  			// Spring这里有个bug:因为它并不能保证configSource肯定被初始化了  			CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);  			if (corsConfiguration != null) {  				boolean isValid = this.processor.processRequest(corsConfiguration, request, response);      				// 若处理后返回false,或者该请求本身就是个Options请求,那后面的Filter也不要处理了~~~~~  				if (!isValid || CorsUtils.isPreFlightRequest(request)) {  					return;  				}  			}  		}    		filterChain.doFilter(request, response);  	}  }

它的工作完全委托给CorsProcessor去处理的。此Filter可以与DelegatingFilterProxy一起使用,以帮助初始化且可以使用Spring容器内的Bean。注意CorsFilter在框架内部默认是木有配置的,若有需要请自行配置~

CorsFilter属于jar包内的过滤器,在没有web.xml环境下如何配置呢?详见下个章节的示例

@CrossOrigin

Spring MVC提供了此注解来帮助你解决CORS跨域问题,比你使用Filter更加的方便,且能实现更加精细化的控制(一般可以和CorsFilter一起来使用,效果更佳)。 Spring Web MVCSpring WebFluxRequestMappingHandlerMapping里都是支持此注解的,该注解配置参数的原理可参考CorsConfiguration

// @since 4.2 可使用在类上和方法上  @Target({ ElementType.METHOD, ElementType.TYPE })  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface CrossOrigin {    	// 下面4个属性在5.0后都被此方法所代替 因为此方法默认会被执行  	/** @deprecated as of Spring 5.0, in favor of {@link CorsConfiguration#applyPermitDefaultValues} */  	@Deprecated  	String[] DEFAULT_ORIGINS = { "*" };  	@Deprecated  	String[] DEFAULT_ALLOWED_HEADERS = { "*" };  	@Deprecated  	boolean DEFAULT_ALLOW_CREDENTIALS = false;  	@Deprecated  	long DEFAULT_MAX_AGE = 1800;    	// 若需要通配,可写*  	@AliasFor("origins")  	String[] value() default {};  	@AliasFor("value")  	String[] origins() default {};    	String[] allowedHeaders() default {};  	String[] exposedHeaders() default {};  	RequestMethod[] methods() default {};  	String allowCredentials() default "";  	long maxAge() default -1; // 负值意味着不生效 By default this is set to {@code 1800} seconds (30 minutes)  }

此注解可以标注在Controller上和方法上,若都有标注将会有combine的效果。

CorsRegistry / CorsRegistration

这两个类是Spring MVC提供出来便于进行global全局配偶的,它是基于URL pattern配置的。

public class CorsRegistry {  	// 保存着全局的配置,每个CorsRegistration就是URL pattern和CorsConfiguration配置  	private final List<CorsRegistration> registrations = new ArrayList<>();    	// 像上面List添加一个全局配置(和pathPattern绑定)  	// 它使用的是new CorsRegistration(pathPattern)  	// 可见使用配置是默认配置:new CorsConfiguration().applyPermitDefaultValues()  	// 当然它CorsRegistration return给你了,你还可以改(配置)的~~~~  	public CorsRegistration addMapping(String pathPattern) {  		CorsRegistration registration = new CorsRegistration(pathPattern);  		this.registrations.add(registration);  		return registration;  	}    	// 这个就比较简单了:把当前List专程Map。key就是PathPattern~~~~  	protected Map<String, CorsConfiguration> getCorsConfigurations() {  		Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());  		for (CorsRegistration registration : this.registrations) {  			configs.put(registration.getPathPattern(), registration.getCorsConfiguration());  		}  		return configs;  	}  }

对于CorsRegistration这个类,它就是持有pathPatternCorsConfiguration config两个属性,它特特点是提供了allowedMethods/allowedHeaders...等方法,提供钩子方便我们对CorsConfiguration进行配置,源码很简单略。

这两个类虽然简单,但是在@EnableWebMvc里扩展配置时使用得较多,参见下个章节对WebMvcConfigurer扩展使用和配置

总结

本文内容主要介绍Spring MVC它对CORS支持的那些类,为我们生产是灵活的使用Spring MVC解决CORS问题提供理论基础。下个章节也是本系列的最后一个章节,将具体介绍Spring MVC中对CORS的实践。