【SpringMVC】與權限攔截器衝突致使的Cors跨域設置失效問題

問題描述

前端域名FE.com向後端域名BE.com分別請求訪問優惠券的列表和提交新增的優惠券,API設計所用的Method分別爲GetPost,結果爲前一次訪問成功然後一次訪問失敗。這兩次請求都是跨域請求,其中請求1包含一個Get請求,請求2本應該包含一個Options請求和一個Post請求,可是隻發生了Options請求。前端

後端Cors配置

CORS使用SpringMVC自帶的<mvc:cors>標籤全局配置,權限檢測則實現自定義的攔截器校驗Cookie中的Token信息。java

<mvc:cors>
        <mvc:mapping allow-credentials="true" allowed-headers="Content-Type" 
        allowed-methods="POST, GET, OPTIONS, DELETE, PUT, HEAD" 
        allowed-origins="http://test.i.meituan.com" 
        max-age="3600" path="/**"/>
</mvc:cors>
<mvc:interceptors>
   <mvc:interceptor>
       <mvc:mapping path="/ajax/shop/**"/> <!--sso回調處理-->
       <bean class="com.dianping.orderdish.framework.interceptor.ShopSsoInterceptor"/>
   </mvc:interceptor>
</mvc:interceptors>

在複雜請求的狀況下(即請求2),因爲預檢請求不會包含Cookie信息(瀏覽器自己的實現決定其是否發送Cookie,前端沒法控制,而且Chrome是不發送的),所以被權限攔截器提早結束,沒有輸出包含指定頭部信息的響應。而一個被瀏覽器認爲合格的預檢請求響應必須包含以下的Http頭部。git

Access-Control-Allow-Origin: http://test.i.meituan.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400

爲何會發生提早結束這種狀況?能夠對dispatch()方法進行分析。github

Handler和攔截器的執行順序

DispatchServlet.doDispatch()方法是SpringMVC的核心入口方法,分析發現全部的攔截器的preHandle()方法的執行都在實際Handler的方法(好比某個API對應的業務方法)以前,其中任意攔截器返回false都會跳事後續全部處理過程。而SpringMVC對預檢請求的處理則在PreFlightHandler.handleRequest()中處理,在整個處理鏈條中出於後置位。因爲預檢請求中不帶Cookie,所以先被權限攔截器攔截。ajax

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
//省略代碼
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

@CrossCors時序分析

除了在XML中設置全局的CORS配置,Spring提供了@CrossCors,能夠註解在類和方法上,是一種更細粒度的配置方法。SpringMVC把其處理細節包裝在getHandler(processedRequest);中,具體邏輯能夠簡述爲若是請求是預檢請求,則返回PreFlightHander;不然在攔截器末尾添加一個CorsInterceptor。所以能夠看出,@CrossCors相關的執行和全局的<mvc:cors>相似,也是滯後於權限攔截器的。spring

解決方案

方案1:使用Spring-Web自帶的CorsFilter

因爲CorsFilter是定義在Web容器中的過濾器(實現了javax.servlet.Filter),所以其執行順序先於Servlet,而SpringMVC的入口是DispatchServlet,所以該Filter會先於SpringMVC的全部攔截器執行。分析代碼可知,CorsFilter會獲取單個請求對應的Cors配置作相應的處理。所以能夠和<mvc:cors>很好的結合,不須要增長額外的代碼。(勘誤:CorsFilter的構造須要CorsConfigurationSource實例,而且發生在SpringMVC配置文件解析以前,所以只能放在Spring的配置文件中,不然會發生找不到bean的異常;又因爲<mvc:cors>的解析和Spring核心的解析不共享相同的ParserContext上下文,所以SpringMVC的跨域設置不能植入到CorsFilter中)後端

if (CorsUtils.isCorsRequest(request)) {
    CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
    if (corsConfiguration != null) {
        boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
        if (!isValid || CorsUtils.isPreFlightRequest(request)) {
            return;
        }
    }
}
filterChain.doFilter(request, response);

方案2:本身實現Interceptor

與方案1相似,把插入Http頭的功能實現爲SpringMVC的攔截器,而後在<mvc:interceptors>中聲明爲第一順位。跨域

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   if (request.getHeader(HttpHeaders.ORIGIN) != null) {
        response.addHeader("Access-Control-Allow-Origin", "http://test.i.meituan.com");
        response.addHeader("Access-Control-Allow-Credentials", "true");
        response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT, HEAD");
        response.addHeader("Access-Control-Allow-Headers", "Content-Type");
        response.addHeader("Access-Control-Max-Age", "3600");
   }
   return true;
}

方案3:加強自定義Interceptor

利用AOP環繞加強全部自定義攔截器的preHandle()方法,令其跳過預檢請求的攔截。瀏覽器

@Around(value = "cut()")
public Object processTx(ProceedingJoinPoint jp) throws Throwable {
   HttpServletRequest request = (HttpServletRequest) jp.getArgs()[0];
   if (request != null && CorsUtils.isPreFlightRequest(request)) {
       return true;
   } else {
       return jp.proceed();
   }
}

Reference

  1. HTTP訪問控制(CORS)
  2. Token endpoint should permit all HTTP OPTIONS requests to support CORS. #330
  3. how can i convince spring 4.2 to pass options request through to the controller
相關文章
相關標籤/搜索