博客 / 詳情

返回

Spring Security 6.x 一文快速搞懂配置原理

一、基本概念

Spring Security框架看似比較複雜,但説到底,框架中的各種安全功能,基本上也就是一個個Filter(javax.servlet.Filter)組成的所謂“過濾器鏈”實現的,這些Filter以職責鏈的設計模式組織起來,環環相扣,不過在剛接觸Spring Security框架時不必盯着每個Filter着重去研究,我們首要的目的是學會如何對Spring Security進行配置,很多人,特別是新手,在看過官方文檔中配置示例代碼(如下所示)之後,在沒有足夠背景知識的情況下,都會對這個http.build()方法感到莫名的困惑,想要定製開發也不知從何下手,本文主要對整個Spring Security配置過程做一定的剖析,希望可以對學習Spring Sercurity框架的同學所有幫助。

版本説明:下文所貼出的各段源碼均源自6.2.3版本,但其實5.7以上的各個版本,跟配置相關的代碼基本相同,變動不算太大
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults());
    return http.build();
}

概況地説,HttpSecurity的配置過程,主要就是向這個SecurityFilterChian中添加不同功能的Filter對象,為了方便後文理解,首先來看一下其中涉及的幾個重要的接口和類(關係如下圖)

接口
  • SecurityBuilder:頂層接口,定義了抽象的泛型構造器方法——build()
  • SecurityConfigurer:頂層接口,用來定義配置類的通用方法,每個Filter都是由特定的SecurityConfigurer的實現類構建出來並添加到FilterChain中的
  • SecurityFilterChain:頂層接口,即過濾器鏈,定義了獲取List<Filter>的方法,以及matches,用於判斷某個請求是否滿足進鏈的條件
  • HttpSecurityBuilder:繼承SecurityBuilder,定義了構建SecurityFilterChain過程中的各種輔助方法,如添加Filter到SecurityFilterChain,獲取SecurityConfigurer配置實現類等
  • AbstractSecurityBuilder:頂層的抽象父類,它沒有實現build具體的邏輯,實際交由doBuild方法實現,只是用CAS對doBuild過程進行了併發控制
  • AbstractConfiguredSecurityBuilder:繼承了AbstractSecurityBuilder,它內部維護了一個SecurityConfigurer的列表,實現了doBuild方法,確立了整個構建的流程
  • HttpSecurity 作為final實現類,它主要面向開發者,我們在開發過程中就是用它提供的一系列的配置入口,方便開發者對SecurityFilterChain中不同的Filter進行定製,包括添加自定義的Filter,關閉某些Filter,或擴展原來Filter的能力等等

二、基本流程

接下來,重點分析一下AbstractConfiguredSecurityBuilder類,整個構建過程圍繞doBuild方法,主要分為:

  1. 初始化,包括beforeInit和init方法,其中beforeInit是擴展用的鈎子方法,默認實現為空
  2. 配置,包括beforeConfigure和configure方法,其中beforeConfigure是擴展用的鈎子方法,默認實現為空
  3. 構造,即performBuild方法,具體由HttpSecurity實現,主要是對Filter進行排序,並最終返回DefaultSecurityFilterChain的實例

而在AbstractConfiguredSecurityBuilder中維護了一個Map<Class,List<SecurityConfigure>>對象,用於緩存各種SecurityConfigure的實現類,當調用init和configure時,實際上就會遍歷這個Map所有configurer,依次調用對應方法,通常就是在configure方法中,將Filter加入到FilterChain中(下文詳述

protected final O doBuild() throws Exception {
    synchronized (this.configurers) {
       this.buildState = BuildState.INITIALIZING;
       beforeInit();
       init();
       this.buildState = BuildState.CONFIGURING;
       beforeConfigure();
       configure();
       this.buildState = BuildState.BUILDING;
       O result = performBuild();
       this.buildState = BuildState.BUILT;
       return result;
    }
}
 
 private void configure() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    for (SecurityConfigurer<O, B> configurer : configurers) {
       configurer.configure((B) this);
    }
}

那麼,這些SecurityConfigure實例則是如何添加上述的Map中的?可以在HttpSecurityConfiguration找到相關的實現,源碼如下

@Bean(HTTPSECURITY_BEAN_NAME)
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception {
    LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context);
    AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(
          this.objectPostProcessor, passwordEncoder);
    authenticationBuilder.parentAuthenticationManager(authenticationManager());
    authenticationBuilder.authenticationEventPublisher(getAuthenticationEventPublisher());
    HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    WebAsyncManagerIntegrationFilter webAsyncManagerIntegrationFilter = new WebAsyncManagerIntegrationFilter();
    webAsyncManagerIntegrationFilter.setSecurityContextHolderStrategy(this.securityContextHolderStrategy);
    // @formatter:off
    http
       .csrf(withDefaults())
       .addFilter(webAsyncManagerIntegrationFilter)
       .exceptionHandling(withDefaults())
       .headers(withDefaults())
       .sessionManagement(withDefaults())
       .securityContext(withDefaults())
       .requestCache(withDefaults())
       .anonymous(withDefaults())
       .servletApi(withDefaults())
       .apply(new DefaultLoginPageConfigurer<>());
    http.logout(withDefaults());
    // @formatter:on
    applyCorsIfAvailable(http);
    applyDefaultConfigurers(http);
    return http;
}

可以看到在構造過程中(那段很長鏈式配置),已經幫我們添加了若干SecurityConfgurer實例,因此我們在使用配置SecurityFilterChain時,僅需要很少的配置就可以得到一個完整的具備基本功能的SecurityFilterChain,當然我們也可以利用這些配置項做很多定製開發。事實上,HttpSecurity大約提供了24個Filter相關的Configurer配置方法,其中11個Filter是默認加載的,整理成表格:

Filters.jpg

如果我們不對HttpSecurity做任何改動的話,默認得到的SecurityFilterChain是如下這樣的,先了解大概,後續還針對部分重要的filter做深入分析。

org.springframework.security.web.session.DisableEncodeUrlFilter
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextHolderFilter
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.web.filter.CorsFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
org.springframework.security.web.access.ExceptionTranslationFilter

三、SecurityConfigurer舉例

這裏還是以Spring Security官方文檔中配置的示例代碼為例,配置代碼只需幾行,比較優雅,這種設計是值得學習的,儘量讓複雜的配置邏輯封裝起來,讓開發者在使用時,只需要關注業務邏輯即可

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
            .authorizeHttpRequests(authorize -> authorize
                    .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .httpBasic(withDefaults());
    return http.build();
}

Spring Security提供了兩種方式進行配置,一種就是示例代碼中,即利用lambda表達式實現配置邏輯,這是5.5版本引入的,在這之前是使用無參的方法獲取配置對象 ,然後進行鏈式的配置,如上述示例代碼可以改寫為

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.
            authorizeHttpRequests().anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
    return http.build();

}

以authorizeHttpRequests為例看一下源碼實現,可以看到兩種方式大同小異,只是使用了Customer函數式接口進行了封裝

@Deprecated(since = "6.1", forRemoval = true)
public AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizeHttpRequests()
       throws Exception {
    ApplicationContext context = getContext();
    return getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry();
}

public HttpSecurity authorizeHttpRequests(
       Customizer<AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry> authorizeHttpRequestsCustomizer)
       throws Exception {
    ApplicationContext context = getContext();
    authorizeHttpRequestsCustomizer
       .customize(getOrApply(new AuthorizeHttpRequestsConfigurer<>(context)).getRegistry());
    return HttpSecurity.this;
}

其中getOrApply方法,用於獲取到Configurer的具體實例(上文中提到過在AbstractConfiguredSecurityBuilder中維護了一個Configurers的Map,這些Configurer實例便是從這個Map獲取的)
不過第二種寫法,根據源碼的註釋,應該會在Spring Security 7版本里面移除,所以還是要適應這種Customizer參數的配置方法。
上面authorizeHttpRequests方法返回的是AuthorizeHttpRequestsConfigurer類中的AuthorizationManagerRequestMatcherRegistry對象,可以先看一下AuthorizeHttpRequestsConfigurer中的configure方法

public void configure(H http) {
    AuthorizationManager<HttpServletRequest> authorizationManager = this.registry.createAuthorizationManager();
    AuthorizationFilter authorizationFilter = new AuthorizationFilter(authorizationManager);
    authorizationFilter.setAuthorizationEventPublisher(this.publisher);
    authorizationFilter.setShouldFilterAllDispatcherTypes(this.registry.shouldFilterAllDispatcherTypes);
    authorizationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
    http.addFilter(postProcess(authorizationFilter));
}

這裏創建了一個AuthorizationFilter,並添加到HttpSecurity的List<Filter>中,而AuthorizationManagerRequestMatcherRegistry則又是一個構造器模式實現的配置類,主要功能就是配置一些權限攔截的具體邏輯,如哪些地址需要什麼角色訪問等,這裏就不展開了。
再看一下formLogin的例子

public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
    formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
    return HttpSecurity.this;
}

formLogin方法實際上創建FormLoginConfigurer的示例,該類主要用於創建UsernamePasswordAuthenticationFilter,即默認的用户名密碼認證的過濾器,其中的configure方法由父類AbstractAuthenticationFilterConfigurer實現,源碼如下,雖然這個方法有點長,但基本是圍繞配置UsernamePasswordAuthenticationFilter實例而展開(this.authFilter就是UsernamePasswordAuthenticationFilter的實例,它在FormLoginConfigurer的構造函數中創建出來),主要就是創建用户認證所用到的一些基本組件,例如AuthenticationManager用於封裝不同的用户認證方式(如用户名密碼),AuthenticationSuccessHandler用於封裝認證成功後執行的操作,AuthenticationFailureHandler用於封裝認證失敗後執行的操作等等

@Override
public void configure(B http) throws Exception {
    PortMapper portMapper = http.getSharedObject(PortMapper.class);
    if (portMapper != null) {
       this.authenticationEntryPoint.setPortMapper(portMapper);
    }
    RequestCache requestCache = http.getSharedObject(RequestCache.class);
    if (requestCache != null) {
       this.defaultSuccessHandler.setRequestCache(requestCache);
    }
    this.authFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
    this.authFilter.setAuthenticationSuccessHandler(this.successHandler);
    this.authFilter.setAuthenticationFailureHandler(this.failureHandler);
    if (this.authenticationDetailsSource != null) {
       this.authFilter.setAuthenticationDetailsSource(this.authenticationDetailsSource);
    }
    SessionAuthenticationStrategy sessionAuthenticationStrategy = http
       .getSharedObject(SessionAuthenticationStrategy.class);
    if (sessionAuthenticationStrategy != null) {
       this.authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
    }
    RememberMeServices rememberMeServices = http.getSharedObject(RememberMeServices.class);
    if (rememberMeServices != null) {
       this.authFilter.setRememberMeServices(rememberMeServices);
    }
    SecurityContextConfigurer securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
    if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
       SecurityContextRepository securityContextRepository = securityContextConfigurer
          .getSecurityContextRepository();
       this.authFilter.setSecurityContextRepository(securityContextRepository);
    }
    this.authFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
    F filter = postProcess(this.authFilter);
    http.addFilter(filter);
}

通過上面兩個源碼示例,可以看到配置Filter的過程其實並不複雜,當我們在研究Spring Security不同過濾器功能時,可以參考源碼中configure的配置過程,分析它們有哪些配置項,這些配置點主要能提供什麼樣能力的等,從而打開思路,快速實現不同的定製需求。

四、總結

最後做一個簡單的總結,如圖所示:

  1. 從HttpSercurityConfiguration定義HttpSecurity的Bean對象開始,便向HttpSecurity中添加了若干SecurityConfigurer對象,另外我們可以在自定義的配置類中對其進行一些定製調整
  2. 然後當調用HttpSecurity#Build()方法時,就會將取得所有SecurityConfigurer進行遍歷,依次調用對應的init和configurer方法,而在configurer方法中,創建出各種功能的Filter實例,並添加到List<OrderedFilter>列表中
  3. 最後通過performBuild方法,將List<OrderedFilter>進行排序,並創建出DefaultSecurityFilterChian
    至此整個過濾器鏈的構建就完成了。
    1.jpg
user avatar jingsewu 頭像 snower 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.