解讀SpringBoot中addCorsMappings配置跨域與攔截器互斥問題的原因
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); } }; }
通過在網(wǎng)上的查詢,發(fā)現(xiàn)了如下解釋
- 但是使用此方法配置之后再使用自定義攔截器時跨域相關配置就會失效。
- 原因是請求經(jīng)過的先后順序問題,當請求到來時會先進入攔截器中,而不是進入Mapping映射中,所以返回的頭信息中并沒有配置的跨域信息。瀏覽器就會報跨域異常。
然后參考了網(wǎng)上給出的方法,重新引入了跨域過濾器配置,解決了這個問題。
那最終這個問題產(chǎn)生的原因是什么的,真的如上訴所說嗎,我通過調試與研究源碼,找了原因。
在springMvc中,我們都知道路徑的映射匹配是通過DispatcherServlet這個類來實現(xiàn)的,最終的函數(shù)執(zhí)行在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)這幾句代碼,基本上整個映射執(zhí)行的邏輯就明了了:
- (1)根據(jù)請求request獲取執(zhí)行器鏈(包括攔截器和最終執(zhí)行方法Handler)
- (2)根據(jù)Handler獲取handlerAdapter;
- (3)執(zhí)行執(zhí)行器鏈中的攔截方法(preHandle);
- (4)執(zhí)行handler方法;
- (5)執(zhí)行執(zhí)行器鏈中的攔截方法(postHandle);
在這個函數(shù)中我們并沒有看到什么時候執(zhí)行addCorsMappings這一配置內(nèi)容,那它到底是什么時候添加的呢,那就需要仔細分析步驟(1)了:獲取整個執(zhí)行器鏈。
通過調試定位,我發(fā)現(xiàn)getHandle()最終執(zhí)行的AbstractHandlerMapping這個類的函數(shù)
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; }
這個函數(shù)中我也標記了(1)、(2)、(3)這三條語句:
- (1)獲取request所需執(zhí)行的handler,具體邏輯不再細說,有興趣的可以參考我的另一篇文章;
- (2)獲取執(zhí)行器鏈,簡單來說就是把具體的執(zhí)行器和整個攔截器鏈組成一個鏈隊形,方便后續(xù)執(zhí)行;
- (3)這個就是關鍵點,可能有的同學已經(jīng)看明白了,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是否是預檢請求(不明白什么是預檢請求的可以自身搜索相關解釋,很多,不再贅述),是預檢請求則生成個預檢執(zhí)行器PreFlightHandler,然后在doDispatch函數(shù)(4)中執(zhí)行;
否則生成一個跨域攔截器加入攔截器鏈中,最終再doDispatch函數(shù)(3)處執(zhí)行,而因為攔截器是順序執(zhí)行的,如果前面執(zhí)行失敗異常返回后,后面的則不再執(zhí)行。
所以當跨越請求在攔截器那邊處理后就異常返回了,那么響應的response報文頭部關于跨域允許的信息就沒有被正確設置,導致瀏覽器認為服務不允許跨域,而造成錯誤;而當我們使用過濾器時,過濾器先于攔截器執(zhí)行,那么無論是否被攔截,始終有允許跨域的頭部信息,就不會出問題了。
另注:
對于預檢請求,一般token驗證時是不會攔截此請求的,因為預檢請求不會附帶任何參數(shù)信息,也就沒有所需的token信息,所以攔截時需過濾預檢請求
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
MyBatis中的@SelectProvider注解源碼分析
這篇文章主要介紹了MyBatis中的@SelectProvider注解源碼分析,@SelectProvider功能就是用來單獨寫一個class類與方法,用來提供一些xml或者注解中不好寫的sql,今天就來說下這個注解的具體用法與源碼,需要的朋友可以參考下2024-01-01java獲取登錄者IP和登錄時間的兩種實現(xiàn)代碼詳解
這篇文章主要介紹了java獲取登錄者IP和登錄時間的實現(xiàn)代碼,本文通過兩種結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-07-07