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的實踐。