Spring Boot中的跨域,爲何加了 Spring Security 就失效了呢?

圖片

本文地址:sf.gg/a/1190000019485883前端

關於 Spring Boot 中的跨域問題,鬆哥以前寫過一篇文章:Spring Boot 中三種跨域場景總結,不過當時只是寫了用法,沒有講原理,其實原理也不難,最近恰好看到一篇文章很不錯,和你們分享一下。後端

鬆哥稍微總結一下文章內容:跨域

  1. Spring Boot 中處理跨域有三種方式,@CrossOrigin 註解、WebMvcConfigurer.addCorsMappings 以及 CorsFilter。
  2. 前兩種本質上經過 CorsInterceptor 觸發調用,後者是一個普通的 Filter。
  3. Filter 先於 DispatcherServlet 執行,DispatcherServlet 先於 Interceptor 執行。
  4. 引入 Spring Security 後,Spring Security 過濾器會攔截下跨域的預檢請求,此時只要放對 CorsFilter 的位置使其先於 Spring Security 過濾器執行,就能解決問題。

如下是正文:瀏覽器

做爲一個後端開發,咱們常常遇到的一個問題就是須要配置 CORS,好讓咱們的前端可以訪問到咱們的 API,而且不讓其餘人訪問。而在 Spring 中,咱們見過不少種 CORS 的配置,不少資料都只是告訴咱們能夠這樣配置、能夠那樣配置,可是這些配置有什麼區別?緩存

1.CORS 是什麼

首先咱們要明確,CORS 是什麼,以及規範是如何要求的。這裏只是梳理一下流程。安全

CORS 全稱是 Cross-Origin Resource Sharing,直譯過來就是跨域資源共享。要理解這個概念就須要知道域、資源和同源策略這三個概念。服務器

瞭解了這三個概念,咱們就能理解爲何有 CORS 規範了:從站點 A 請求站點 B 的資源的時候,因爲瀏覽器的同源策略的影響,這樣的跨域請求將被禁止發送;爲了讓跨域請求可以正常發送,咱們須要一套機制在不破壞同源策略的安全性的狀況下、容許跨域請求正常發送,這樣的機制就是 CORS。cookie

2.預檢請求

在 CORS 中,定義了一種預檢請求,即 preflight request,當實際請求不是一個 簡單請求 時,會發起一次預檢請求。預檢請求是針對實際請求的 URL 發起一次 OPTIONS 請求,並帶上下面三個 headers :mvc

若是服務器端 CORS 驗證失敗,則會返回客戶端錯誤,即 4xx 的狀態碼。app

不然,將會請求成功,返回 200 的狀態碼,並帶上下面這些 headers:

2.1 小結

到這裏, 咱們就知道了跨域請求會經歷的故事:

  1. 訪問另外一個域的資源。
  2. 有可能會發起一次預檢請求(非簡單請求,或超過了 Max-Age)。
  3. 發起實際請求。

接下來,咱們看看在 Spring 中,咱們是如何讓 CORS 機制在咱們的應用中生效的。

3.三種配置的方式

Spring 提供了多種配置 CORS 的方式,有的方式針對單個 API,有的方式能夠針對整個應用;有的方式在一些狀況下是等效的,而在另外一些狀況下卻又出現不一樣。咱們這裏例舉幾種典型的方式來看看應該如何配置。

假設咱們有一個 API:

@RestController
class HelloController {
    @GetMapping("hello")
    fun hello(): String {
        return "Hello, CORS!"
    }
}

3.1 @CrossOrigin 註解

使用 @CorssOrigin 註解須要引入 Spring Web 的依賴,該註解能夠做用於方法或者類,能夠針對這個方法或類對應的一個或多個 API 配置 CORS 規則:

@RestController
class HelloController {
    @GetMapping("hello")
    @CrossOrigin(origins = ["http://localhost:8080"])
    fun hello(): String {
        return "Hello, CORS!"
    }
}

3.2 實現 WebMvcConfigurer.addCorsMappings 方法

WebMvcConfigurer 是一個接口,它一樣來自於 Spring Web。咱們能夠經過實現它的 addCorsMappings 方法來針對全局 API 配置 CORS 規則:

@Configuration
@EnableWebMvc
class MvcConfigWebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/hello")
                .allowedOrigins("http://localhost:8080")
    }
}

3.3 注入 CorsFilter

CorsFilter 一樣來自於 Spring Web,可是實現 WebMvcConfigurer.addCorsMappings 方法並不會使用到這個類,具體緣由咱們後面來分析。咱們能夠經過注入一個 CorsFilter 來使用它:

@Configuration
class CORSConfiguration {
    @Bean
    fun corsFilter(): CorsFilter {
        val configuration = CorsConfiguration()
        configuration.allowedOrigins = listOf("http://localhost:8080")
        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration("/hello", configuration)
        return CorsFilter(source)
    }
}

注入 CorsFilter 不止這一種方式,咱們還能夠經過注入一個 FilterRegistrationBean 來實現,這裏就不給例子了。

在僅僅引入 Spring Web 的狀況下,實現 WebMvcConfigurer.addCorsMappings 方法和注入 CorsFilter 這兩種方式能夠達到一樣的效果,二選一便可。它們的區別會在引入 Spring Security 以後會展示出來,咱們後面再來分析。

4.Spring Security 中的配置

在引入了 Spring Security 以後,咱們會發現前面的方法都不能正確的配置 CORS,每次 preflight request 都會獲得一個 401 的狀態碼,表示請求沒有被受權。這時,咱們須要增長一點配置才能讓 CORS 正常工做:

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity?) {
        http?.cors()
    }
}

或者,乾脆不實現 WebMvcConfigurer.addCorsMappings 方法或者注入 CorsFilter ,而是注入一個 CorsConfigurationSource ,一樣能與上面的代碼配合,正確的配置 CORS:

@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
    val configuration = CorsConfiguration()
    configuration.allowedOrigins = listOf("http://localhost:8080")
    val source = UrlBasedCorsConfigurationSource()
    source.registerCorsConfiguration("/hello", configuration)
    return source
}

到此,咱們已經看過了幾種典型的例子了,咱們接下來看看 Spring 究竟是如何實現 CORS 驗證的。

5.這些配置有什麼區別

咱們會主要分析實現 WebMvcConfigurer.addCorsMappings 方法和調用 HttpSecurity.cors 方法這兩種方式是如何實現 CORS 的,但在進行以前,咱們要先複習一下 Filter 與 Interceptor 的概念。

5.1 Filter 與 Interceptor

圖片

上圖很形象的說明了 Filter 與 Interceptor 的區別,一個做用在 DispatcherServlet 調用前,一個做用在調用後。

但實際上,它們自己並無任何關係,是徹底獨立的概念。

Filter 由 Servlet 標準定義,要求 Filter 須要在 Servlet 被調用以前調用,做用顧名思義,就是用來過濾請求。在 Spring Web 應用中,DispatcherServlet 就是惟一的 Servlet 實現。

Interceptor 由 Spring 本身定義,由 DispatcherServlet 調用,能夠定義在 Handler 調用先後的行爲。這裏的 Handler ,在多數狀況下,就是咱們的 Controller 中對應的方法。

對於 Filter 和 Interceptor 的複習就到這裏,咱們只須要知道它們會在何時被調用到,就能理解後面的內容了。

5.2 WebMvcConfigurer.addCorsMappings 方法作了什麼

咱們從 WebMvcConfigurer.addCorsMappings 方法的參數開始,先看看 CORS 配置是如何保存到 Spring 上下文中的,而後在瞭解一下 Spring 是如何使用的它們。

5.2.1 注入 CORS 配置

5.2.1.1 CorsRegistry 和 CorsRegistration

WebMvcConfigurer.addCorsMappings 方法的參數 CorsRegistry 用於註冊 CORS 配置,它的源碼以下:

public class CorsRegistry {
    private final List<CorsRegistration> registrations = new ArrayList<>();

    public CorsRegistration addMapping(String pathPattern) {
        CorsRegistration registration = new CorsRegistration(pathPattern);
        this.registrations.add(registration);
        return registration;
    }

    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 這個類,一樣很簡單,咱們看看它的部分源碼:

public class CorsRegistration {
    private final String pathPattern;
    private final CorsConfiguration config;


    public CorsRegistration(String pathPattern) {
        this.pathPattern = pathPattern;
        this.config = new CorsConfiguration().applyPermitDefaultValues();
    }

    public CorsRegistration allowedOrigins(String... origins) {
        this.config.setAllowedOrigins(Arrays.asList(origins));
        return this;
    }
}

不難發現,這個類僅僅保存了一個 pathPattern 字符串和 CorsConfiguration,很好理解,它保存的是一個 pathPattern 對應的 CORS 規則。

在它的構造函數中,調用的 CorsConfiguration.applyPermitDefaultValues 方法則用於配置默認的 CORS 規則:

建立 CorsRegistration 後,咱們能夠經過它的 allowedOrigins、allowedMethods 等方法修改它的 CorsConfiguration,覆蓋掉上面的默認值。

如今,咱們已經經過 WebMvcConfigurer.addCorsMappings 方法配置好 CorsRegistry 了,接下來看看這些配置會在什麼地方被注入到 Spring 上下文中。

5.2.1.2 WebMvcConfigurationSupport

CorsRegistry.getCorsConfigurations 方法,會被 WebMvcConfigurationSupport.getConfigurations 方法調用,這個方法以下:

protected final Map<String, CorsConfiguration> getCorsConfigurations() {
    if (this.corsConfigurations == null) {
        CorsRegistry registry = new CorsRegistry();
        addCorsMappings(registry);
        this.corsConfigurations = registry.getCorsConfigurations();
    }
    return this.corsConfigurations;
}

addCorsMappings(registry) 調用的是本身的方法,由子類 DelegatingWebMvcConfiguration 經過委託的方式調用到 WebMvcConfigurer.addCorsMappings 方法,咱們的配置也由此被讀取到。

getCorsConfigurations 是一個 protected 方法,是爲了在擴展該類時,仍然可以直接獲取到 CORS 配置。而這個方法在這個類裏被四個地方調用到,這四個調用的地方,都是爲了註冊一個 HandlerMapping 到 Spring 容器中。每個地方都會調用 mapping.setCorsConfigurations 方法來接收 CORS 配置,而這個 setCorsConfigurations 方法,則由 AbstractHandlerMapping 提供,CorsConfigurations 也被保存在這個抽象類中。

到此,咱們的 CORS 配置藉由 AbstractHandlerMapping 被注入到了多個 HandlerMapping 中,而這些 HandlerMapping 以 Spring 組件的形式被註冊到了 Spring 容器中,當請求來臨時,將會被調用。

5.2.2 獲取 CORS 配置

還記得前面關於 Filter 和 Interceptor 那張圖嗎?當請求來到 Spring Web 時,必定會到達 DispatcherServlet 這個惟一的 Servlet。

在 DispatcherServlet.doDispatch 方法中,會調用全部 HandlerMapping.getHandler 方法。好巧不巧,這個方法又是由 AbstractHandlerMapping 實現的:

@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    // 省略代碼
    if (CorsUtils.isCorsRequest(request)) {
        CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
        CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
        CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }
    return executionChain;
}

在這個方法中,關於 CORS 的部分都在這個 if 中。咱們來看看最後這個 getCorsHandlerExecutionChain 作了什麼:

protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
        HandlerExecutionChain chain, @Nullable 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;
}

能夠看到:

這裏的 PreFlightHandler 和 CorsInterceptor 都是 AbstractHandlerMapping 的內部類,實現幾乎一致,區別僅僅在於一個是 HttpRequestHandler,一個是 HandlerInterceptor;它們對 CORS 規則的驗證都交由 CorsProcessor 接口完成,這裏採用了默認實現 DefaultCorsProcessor。

DefaultCorsProcessor 則是依照 CORS 標準來實現,並在驗證失敗的時候打印 debug 日誌並拒絕請求。咱們只須要關注一下標準中沒有定義的驗證失敗時的狀態碼:

protected void rejectRequest(ServerHttpResponse response) throws IOException {
    response.setStatusCode(HttpStatus.FORBIDDEN);
    response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
}

CORS 驗證失敗時調用這個方法,並設置狀態碼爲 403。

5.2.3 小結

經過對源碼的研究,咱們發現實現 WebMvcConfigurer.addCorsMappings 方法的方式配置 CORS,會在 Interceptor 或者 Handler 層進行 CORS 驗證。

5.3 HtttpSecurity.cors 方法作了什麼

在研究這個方法的行爲以前,咱們先來回想一下,咱們調用這個方法解決的是什麼問題。

前面咱們經過某種方式配置好 CORS 後,引入 Spring Security,CORS 就失效了,直到調用這個方法後,CORS 規則才從新生效。

下面這些緣由,致使了 preflight request 沒法經過身份驗證,從而致使 CORS 失效:

  1. preflight request 不會攜帶認證信息。
  2. Spring Security 經過 Filter 來進行身份驗證。
  3. Interceptor 和 HttpRequestHanlder 在 DispatcherServlet 以後被調用。
  4. Spring Security 中的 Filter 優先級比咱們注入的 CorsFilter 優先級高。

接下來咱們就來看看 HttpSecurity.cors 方法是如何解決這個問題的。

5.3.1 CorsConfigurer 如何配置 CORS 規則

HttpSecurity.cors 方法中其實只有一行代碼:

public CorsConfigurer<HttpSecurity> cors() throws Exception {
    return getOrApply(new CorsConfigurer<>());
}

這裏調用的 getOrApply 方法會將 SecurityConfigurerAdapter 的子類實例加入到它的父類 AbstractConfiguredSecurityBuilder 維護的一個 Map 中,而後一個個的調用 configure 方法。因此,咱們來關注一下 CorsConfigurer.configure 方法就行了。

@Override
public void configure(H http) throws Exception {
    ApplicationContext context = http.getSharedObject(ApplicationContext.class);

    CorsFilter corsFilter = getCorsFilter(context);
    if (corsFilter == null) {
        throw new IllegalStateException(
                "Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
                        + CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
    }
    http.addFilter(corsFilter);
}

這段代碼很好理解,就是在當前的 Spring Context 中找到一個 CorsFilter,而後將它加入到 http 對象的 filters 中。由上面的 HttpSecurity.cors 方法可知,這裏的 http 對象實際類型就是 HttpSecurity。

5.3.2 getCorsFilter 方法作了什麼

也許你會好奇,HttpSecurity 要如何保證 CorsFilter 必定在 Spring Security 的 Filters 以前調用。可是在研究這個以前,咱們先來看看一樣重要的 getCorsFilter 方法,這裏能夠解答咱們前面的一些疑問。

private CorsFilter getCorsFilter(ApplicationContext context) {
    if (this.configurationSource != null) {
        return new CorsFilter(this.configurationSource);
    }

    boolean containsCorsFilter = context
            .containsBeanDefinition(CORS_FILTER_BEAN_NAME);
    if (containsCorsFilter) {
        return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
    }

    boolean containsCorsSource = context
            .containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
    if (containsCorsSource) {
        CorsConfigurationSource configurationSource = context.getBean(
                CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
        return new CorsFilter(configurationSource);
    }

    boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
            context.getClassLoader());
    if (mvcPresent) {
        return MvcCorsFilter.getMvcCorsFilter(context);
    }
    return null;
}

這是 CorsConfigurer 尋找 CorsFilter 的所有邏輯,咱們用人話來講就是:

  1. CorsConfigurer 本身是否有配置 CorsConfigurationSource,若是有的話,就用它建立一個 CorsFilter。
  2. 在當前的上下文中,是否存在一個名爲 corsFilter 的實例,若是有的話,就把他看成一個 CorsFilter 來用。
  3. 在當前的上下文中,是否存在一個名爲 corsConfigurationSource 的 CorsConfigurationSource 實例,若是有的話,就用它建立一個 CorsFilter。
  4. 在當前上下文的類加載器中,是否存在類 HandlerMappingIntrospector,若是有的話,則經過 MvcCorsFilter 這個內部類建立一個 CorsFilter。
  5. 若是沒有找到,那就返回一個 null,調用的地方最後會拋出異常,阻止 Spring 初始化。

上面的第 二、三、4 步能解答咱們前面的配置爲何生效,以及它們的區別。

註冊 CorsFilter 的方式,這個 Filter 最終會被直接註冊到 Servlet container 中被使用到。

註冊 CorsConfigurationSource 的方式,會用這個 source 建立一個 CorsFiltet 而後註冊到 Servlet container 中被使用到。

而第四步的狀況比較複雜。HandlerMappingIntrospector 是 Spring Web 提供的一個類,實現了 CorsConfigurationSource 接口,因此在 MvcCorsFilter 中,它被直接用於建立 CorsFilter。它實現的 getCorsConfiguration 方法,會經歷:

  1. 遍歷 HandlerMapping。
  2. 調用 getHandler 方法獲得 HandlerExecutionChain。
  3. 從中找到 CorsConfigurationSource 的實例。
  4. 調用這個實例的 getCorsConfiguration 方法,返回獲得的 CorsConfiguration。

因此獲得的 CorsConfigurationSource 實例,實際上就是前面講到的 CorsInterceptor 或者 PreFlightHandler。

因此第四步實際上匹配的是實現 WebMvcConfigurer.addCorsMappings 方法的方式。

因爲在 CorsFilter 中每次處理請求時都會調用 CorsConfigurationSource.getCorsConfiguration 方法,而 DispatcherServlet 中也會每次調用 HandlerMapping.getHandler 方法,再加上這時的 HandlerExecutionChain 中還有 CorsInterceptor,因此使用這個方式相對於其餘方式,作了不少重複的工做。因此 WebMvcConfigurer.addCorsMappings + HttpSecurity.cors 的方式下降了咱們代碼的效率,也許微乎其微,但能避免的狀況下,仍是不要使用。

5.3.3 HttpSecurity 中的 filters 屬性

在 CorsConfigurer.configure 方法中調用的 HttpSecurity.addFilter 方法,由它的父類 HttpSecurityBuilder 聲明,並約定了不少 Filter 的順序。然而 CorsFilter 並不在其中。不過在 Spring Security 中,目前還只有 HttpSecurity 這一個實現,因此咱們來看看這裏的代碼實現就知道 CorsFilter 會排在什麼地方了。

public HttpSecurity addFilter(Filter filter) {
    Class<? extends Filter> filterClass = filter.getClass();
    if (!comparator.isRegistered(filterClass)) {
        throw new IllegalArgumentException("...");
    }
    this.filters.add(filter);
    return this;
}

咱們能夠看到,Filter 會被直接加到 List 中,而不是按照必定的順序來加入的。但同時,咱們也發現了一個 comparator 對象,而且只有被註冊到了該類的 Filter 才能被加入到 filters 屬性中。這個 comparator 又是用來作什麼的呢?

在 Spring Security 建立過程當中,會調用到 HttpSeciryt.performBuild 方法,在這裏咱們能夠看到 filters 和 comparator 是如何被使用到的。

protected DefaultSecurityFilterChain performBuild() throws Exception {
    Collections.sort(filters, comparator);
    return new DefaultSecurityFilterChain(requestMatcher, filters);
}

能夠看到,Spring Security 使用了這個 comparator 在獲取 SecurityFilterChain 的時候來保證 filters 的順序,因此,研究這個 comparator 就能知道在 SecurityFilterChain 中的那些 Filter 的順序是如何的了。

這個 comparator 的類型是 FilterComparator ,從名字就能看出來是專用於 Filter 比較的類,它的實現也並不神祕,從構造函數就能猜到是如何實現的:

FilterComparator() {
    Step order = new Step(INITIAL_ORDER, ORDER_STEP);
    put(ChannelProcessingFilter.class, order.next());
    put(ConcurrentSessionFilter.class, order.next());
    put(WebAsyncManagerIntegrationFilter.class, order.next());
    put(SecurityContextPersistenceFilter.class, order.next());
    put(HeaderWriterFilter.class, order.next());
    put(CorsFilter.class, order.next());
  // 省略代碼
}

能夠看到 CorsFilter 排在了第六位,在全部的 Security Filter 以前,由此便解決了 preflight request 沒有攜帶認證信息的問題。

5.3.4 小結

引入 Spring Security 以後,咱們的 CORS 驗證明際上是依然運行着的,只是由於 preflight request 不會攜帶認證信息,因此沒法經過身份驗證。使用 HttpSecurity.cors 方法會幫助咱們在當前的 Spring Context 中找到或建立一個 CorsFilter 並安排在身份驗證的 Filter 以前,以保證能對 preflight request 正確處理。

6.總結

研究了 Spring 中 CORS 的代碼,咱們瞭解到了這樣一些知識:

轉自 :https://mp.weixin.qq.com/s/ATzkEaRe9GlPVnM_W0Wt3g

相關文章
相關標籤/搜索