有小夥伴們問到這個問題,簡單寫篇文章和大夥聊一下。
一 SecurityFilterChain
首先大夥都知道,Spring Security 裏邊的一堆功能都是通過 Filter 來實現的,無論是認證、RememberMe Login、會話管理、CSRF 處理等等,各種功能都是通過 Filter 來實現的。
所以,我們配置 Spring Security,説白了其實就是配置這些 Filter。
以前舊版繼承自 WebSecurityConfigurerAdapter 類,然後重寫 configure 方法,利用 HttpSecurity 去配置過濾器鏈,這種寫法其實不太好理解,特別對於新手來説,可能半天整不明白到底配置了啥。
現在新版寫法我覺得更加合理,因為直接就是讓開發者自己去配置過濾器鏈,類似下面這樣:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.build();
}
這樣開發者更容易理解,自己是在配置配置 SecurityFilter 過濾器鏈,因為就是要配置這樣一個 Bean。
SecurityFilterChain 是一個接口,這個接口只有一個實現類 DefaultSecurityFilterChain。
DefaultSecurityFilterChain 中有一個 requestMatcher,通過 requestMatcher 可以識別出來哪些請求需要攔截,攔截下來之後,經由該類的另外一個屬性 filters 進行處理,這個 filters 中保存了我們配置的所有 Filter。
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
}
因此,在配置 SecurityFilterChain 這個 Bean 的時候,我們甚至可以按照如下方式來寫:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return new DefaultSecurityFilterChain(new AntPathRequestMatcher("/**"));
}
這個配置就表示攔截所有請求,但是攔截下來之後,這些請求所經過的過濾器為空,即攔截所有請求但是不做任何處理。
我們也可以為 DefaultSecurityFilterChain 對象去配置過濾器鏈,大家看到,它的構造器可以傳入 Filter:
但是,Spring Security 過濾器數量有 30+,我們平日開發使用的也在 15 個左右,這樣一個一個去配置太麻煩了,每個過濾器並非 new 出來就能用了,還要配置各種屬性,所以我們一般不會自己一個一個去配置這些過濾器。
二 SecurityConfigurer
為了簡化 Spring Security 中各個組件的配置,官方推出了 SecurityConfigurer,SecurityConfigurer 在 Spring Security 中扮演着非常重要的角色,其主要作用可以歸納為以下幾點:
- 配置過濾器:在 Spring Security 中,過濾器鏈中的每一個過濾器都是通過 xxxConfigurer 來進行配置的,而這些 xxxConfigurer 實際上都是 SecurityConfigurer 的實現。這意味着 SecurityConfigurer 負責配置和管理安全過濾器鏈中的各個組件。
- 初始化和配置安全構建器:SecurityConfigurer 接口中定義了兩個主要方法:init 和 configure。init 方法用於初始化安全構建器(SecurityBuilder),而 configure 方法則用於配置這個構建器。這兩個方法共同確保了安全組件的正確設置和初始化。
- 提供擴展點:SecurityConfigurer 的三個主要實現類(SecurityConfigurerAdapter、GlobalAuthenticationConfigurerAdapter、WebSecurityConfigurer)為開發者提供了擴展 Spring Security 功能的點。特別是,大部分的 xxxConfigurer 都是 SecurityConfigurerAdapter 的子類,這使得開發者能夠輕鬆地定製和擴展安全配置。
- 構建安全上下文:通過配置和組合不同的 SecurityConfigurer 實現,可以構建一個完整的安全上下文,包括身份驗證、授權、加密等各個方面,從而確保應用程序的安全性。
換句話説,Spring Security 中的各種 Filter,其實都有各自對應的 xxxConfigurer,通過這些 xxxConfigurer 完成了對 Filter 的配置。
舉幾個簡答的例子,如:
- CorsConfigurer 負責配置 CorsFilter
- RememberMeConfigurer 負責配置 RememberMeAuthenticationFilter
- FormLoginConfigurer 負責配置 UsernamePasswordAuthenticationFilter
以上是前置知識。
三 SecurityBuilder
SecurityBuilder 是 Spring Security 框架中的一個核心接口,它體現了建造者設計模式,用於構建和配置安全組件。這個接口並不直接與安全功能如認證或授權的具體實現綁定,而是提供了一種靈活的方式來組織和裝配這些功能組件,特別是在構建安全上下文和過濾器鏈過程中。
核心特點
- 靈活性與模塊化:通過
SecurityBuilder,開發者可以以模塊化的方式添加、移除或替換安全配置中的各個部分,而不需要了解整個安全架構的細節。這促進了代碼的複用性和可維護性。 - 層次結構:在 Spring Security 中,
SecurityBuilder及其子類形成了一個層次結構,允許配置像嵌套娃娃一樣層層深入,每一個層級都可以貢獻自己的配置邏輯,最終形成一個完整的安全配置。 - 安全上下文構建:在認證過程中,
SecurityBuilder用於構造SecurityContext,該上下文中包含了當前認證主體(通常是用户)的詳細信息,以及主體所關聯的權限和角色。 - 過濾器鏈構建:在 Web 應用中,
SecurityBuilder還用於構建FilterChainProxy,這是一個關鍵組件,負責組織和執行一系列的過濾器,這些過濾器負責處理 HTTP 請求的安全性,比如檢查用户是否已經登錄、是否有權限訪問某個資源等。
簡而言之,在 SecurityBuilder 的子類 AbstractConfiguredSecurityBuilder 中,有一個名為 configurers 的集合,這個集合中保存的就是我們前面所説的各種 xxxConfigure 對象。
AbstractConfiguredSecurityBuilder 收集到所有的 xxxConfigure 之後,將來會調用每個 xxxConfigure 的 configure 方法完成過濾器的構建。
另外很重要的一點,HttpSecurity 也是一個 SecurityBuilder。因此,HttpSecurity 其實就是幫我們收集各種各樣的 xxxConfigure,並存入到 configurers 集合中,以備將來構建過濾器使用。
四 HttpSecurity
根據前面的介紹,我們知道,無論我們怎麼配置,最終拿到手的一定是一個 DefaultSecurityFilterChain 對象,因為這是 SecurityFilterChain 的唯一實現類。
所以,HttpSecurity 其實就是在幫我們配置 DefaultSecurityFilterChain,我們看到 HttpSecurity 裏邊就有兩個非常關鍵的屬性:
private List<OrderedFilter> filters = new ArrayList<>();
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
這兩個恰恰就是構建 DefaultSecurityFilterChain 所需的關鍵參數。
事實確實如此,我們看到,在 HttpSecurity#performBuild 方法中,就是利用這兩個參數構建出來了 DefaultSecurityFilterChain:
@Override
protected DefaultSecurityFilterChain performBuild() {
ExpressionUrlAuthorizationConfigurer<?> expressionConfigurer = getConfigurer(
ExpressionUrlAuthorizationConfigurer.class);
AuthorizeHttpRequestsConfigurer<?> httpConfigurer = getConfigurer(AuthorizeHttpRequestsConfigurer.class);
boolean oneConfigurerPresent = expressionConfigurer == null ^ httpConfigurer == null;
this.filters.sort(OrderComparator.INSTANCE);
List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
for (Filter filter : this.filters) {
sortedFilters.add(((OrderedFilter) filter).filter);
}
return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
}
這裏先對 filters 進行排序,然後據此創建出來 DefaultSecurityFilterChain 對象。
那麼問題來了,filters 中的過濾器從何而來呢?
前面我們提到,HttpSecurity 本質上也是一個 SecurityBuilder,我們平時在 HttpSecurity 配置的各種東西,本質上其實就是一個 xxxConfigure,這些 xxxConfigure 被 HttpSecurity 收集起來,最後會遍歷收集起來的 xxxConfigure,調用其 configure 方法,最終獲取過濾器,並將獲取到的過濾器存入到 filters 集合中。
這裏鬆哥以我們最為常見的登錄配置為例來和大家捋一捋這個流程。
新版的登錄配置我們一般按照如下方式來配置:
http.formLogin(f -> f.permitAll());
這個方法如下:
public HttpSecurity formLogin(Customizer<FormLoginConfigurer<HttpSecurity>> formLoginCustomizer) throws Exception {
formLoginCustomizer.customize(getOrApply(new FormLoginConfigurer<>()));
return HttpSecurity.this;
}
從這段源碼可以看到,我們配置裏邊寫的 lambda,其實就是在配置 FormLoginConfigurer 對象。
getOrApply 方法主要主要有兩方面的作用:
- 確保這個 Bean 不會重複配置。
- 如果該 Bean 是第一次配置,那麼將該對象添加到 SecurityBuilder 中(其實就是 HttpSecurity 對象自身),這個 SecurityBuilder 裏邊保存了所有配置好的 xxxConfigure 對象,將來在過濾器鏈構建的時候,會去遍歷這些 xxxConfigure 對象並調用其 configure 方法,完成過濾器的構建。
在 FormLoginConfigurer#init 方法中,完成了和登錄相關過濾器的配置,如:
- 登錄請求處理過濾器(構造方法中完成的)
- 註銷過濾器的配置
- 登錄失敗端點配置
- 登錄頁面的配置
這裏涉及到的屬性都會在對應的 xxxConfigurer 中完成配置。
當過濾器鏈開始構建的時候,會調用到所有 xxxConfigurer 的 configure 方法,在這個方法中,最終完成相關過濾器的創建,並將之添加到 HttpSecurity 的 filters 屬性這個集合中。
FormLoginConfigurer 的 configure 方法在其父類中,如下:
@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);
}
關鍵在最後這一句 http.addFilter(filter);,這句代碼將配置好的過濾器放到了 HttpSecurity 的 filters 集合中。
其他的也都類似,例如配置 CorsFilter 的 CorsConfigurer#configure 方法,如下:
@Override
public void configure(H http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
CorsFilter corsFilter = getCorsFilter(context);
http.addFilter(corsFilter);
}
這些被添加到 HttpSecurity 的 filters 屬性上的 Filter,最終就成為了創建 DefaultSecurityFilterChain 的原材料。
類似的道理,我們也可以分析出 disable 方法的原理,例如我們要關閉 csrf,一般配置如下:
.csrf(c -> c.disable());
那麼很明顯,這段代碼其實就是調用了 CsrfConfigurer 的 disable 方法:
public B disable() {
getBuilder().removeConfigurer(getClass());
return getBuilder();
}
該方法直接從 SecurityBuilder 的 configurers 集合中移除了 CsrfConfigurer,所以導致最終調用各個 xxxConfigure 的 configure 方法的時候,沒有 CsrfConfigurer#configure 了,就導致 csrf 過濾器沒有配置上,進而 CSRF filter 失效。
如果小夥伴們想要徹底學會 Spring Security,那麼不妨看看我最近剛剛錄完的 Spring Security+OAuth2 教程。