天天看點

SpringBoot中addCorsMappings配置跨域與攔截器互斥問題的原因研究

如題,前兩天在做前後端分離項目時,碰到了這個問題,登入token驗證的攔截器使項目中配置的跨域配置失效,導緻浏覽器抛出跨域請求錯誤,跨域配置如下:

public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                        registry.addMapping("/**")
                        .allowedOrigins(origins)
                        .allowedHeaders("*")
                        .allowCredentials(true)
                        .allowedMethods("*")
                        .maxAge(3600);
            }
        };
    }
           

通過在網上的查詢,發現了如下解釋

但是使用此方法配置之後再使用自定義攔截器時跨域相關配置就會失效。

原因是請求經過的先後順序問題,當請求到來時會先進入攔截器中,而不是進入Mapping映射中,是以傳回的頭資訊中并沒有配置的跨域資訊。浏覽器就會報跨域異常。

參見 https://blog.csdn.net/weixin_33958585/article/details/88678712

然後參考了網上給出的方法,重新引入了跨域過濾器配置,解決了這個問題。

那最終這個問題産生的原因是什麼的,真的如上訴所說嗎,我通過調試與研究源碼,找了原因。

在springMvc中,我們都知道路徑的映射比對是通過DispatcherServlet這個類來實作的,最終的函數執行在doDispatch()這個方法中:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request. 
		(1)mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null || mappedHandler.getHandler() == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
		(2)HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (logger.isDebugEnabled()) {
						logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
					}
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

		(3)if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
		(4)mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
		(5)mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
           

在這個類中,我們關注(1)-(5)這幾句代碼,基本上整個映射執行的邏輯就明了了:

(1)根據請求request擷取執行器鍊(包括攔截器和最終執行方法Handler)

(2)根據Handler擷取handlerAdapter;

(3)執行執行器鍊中的攔截方法(preHandle);

(4)執行handler方法;

(5)執行執行器鍊中的攔截方法(postHandle);

在這個函數中我們并沒有看到什麼時候執行addCorsMappings這一配置内容,那它到底是什麼時候添加的呢,那就需要仔細分析步驟(1)了:擷取整個執行器鍊。

通過調試定位,我發現getHandle()最終執行的AbstractHandlerMapping這個類的函數

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
(1)Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		if (handler == null) {
			return null;
		}
		// Bean name or resolved handler?
		if (handler instanceof String) {
			String handlerName = (String) handler;
			handler = getApplicationContext().getBean(handlerName);
		}

(2)HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		if (CorsUtils.isCorsRequest(request)) {
			CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
(3)	executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
		return executionChain;
	}
           

這個函數中我也标記了(1)、(2)、(3)這三條語句:

(1)擷取request所需執行的handler,具體邏輯不再細說,有興趣的可以參考我的另一篇部落格https://blog.csdn.net/huangyaa729/article/details/87862418;

(2)擷取執行器鍊,簡單來說就是把具體的執行器和整個攔截器鍊組成一個鍊隊形,友善後續執行;

(3)這個就是關鍵點,可能有的同學已經看明白了,addCorsMapping配置就是在這塊引入的;

進入這個方法後,一切都明了了;

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
			HandlerExecutionChain chain, CorsConfiguration config) {

		if (CorsUtils.isPreFlightRequest(request)) {
			HandlerInterceptor[] interceptors = chain.getInterceptors();
			chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
		}
		else {
			chain.addInterceptor(new CorsInterceptor(config));
		}
		return chain;
	}
           

先判斷request是否是預檢請求(不明白什麼是預檢請求的可以自身搜尋相關解釋,很多,不再贅述),是預檢請求則生成個預檢執行器PreFlightHandler,然後在doDispatch函數(4)中執行;

否則生成一個跨域攔截器加入攔截器鍊中,最終再doDispatch函數(3)處執行,而因為攔截器是順序執行的,如果前面執行失敗異常傳回後,後面的則不再執行。

是以當跨越請求在攔截器那邊處理後就異常傳回了,那麼響應的response封包頭部關于跨域允許的資訊就沒有被正确設定,導緻浏覽器認為服務不允許跨域,而造成錯誤;而當我們使用過濾器時,過濾器先于攔截器執行,那麼無論是否被攔截,始終有允許跨域的頭部資訊,就不會出問題了。

另注:對于預檢請求,一般token驗證時是不會攔截此請求的,因為預檢請求不會附帶任何參數資訊,也就沒有所需的token資訊,是以攔截時需過濾預檢請求