架構
Filter 回顧
Spring Security 的 Servlet 支援基於 Servlet Filter,因此首先了解 Filter 的一般作用會很有幫助。下圖展示了單個 HTTP 請求的處理器典型分層。

客戶端嚮應用傳送請求,容器建立一個 FilterChain
,其中包含根據請求 URI 的路徑應處理 HttpServletRequest
的 Filter
例項和 Servlet
。在 Spring MVC 應用中,該 Servlet
是 DispatcherServlet
的一個例項。最多一個 Servlet
可以處理單個 HttpServletRequest
和 HttpServletResponse
。然而,可以使用不止一個 Filter
來
-
阻止下游
Filter
例項或Servlet
的呼叫。在這種情況下,Filter
通常負責寫入HttpServletResponse
。 -
修改下游
Filter
例項和Servlet
使用的HttpServletRequest
或HttpServletResponse
。
Filter
的強大之處在於傳遞給它的 FilterChain
。
FilterChain
使用示例-
Java
-
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// do something before the rest of the application
chain.doFilter(request, response); // invoke the rest of the application
// do something after the rest of the application
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
// do something before the rest of the application
chain.doFilter(request, response) // invoke the rest of the application
// do something after the rest of the application
}
由於 Filter
隻影響下游的 Filter
例項和 Servlet
,因此每個 Filter
的呼叫順序極其重要。
DelegatingFilterProxy
Spring 提供了一個名為 DelegatingFilterProxy
的 Filter
實現,它允許連線 Servlet 容器的生命週期與 Spring 的 ApplicationContext
。Servlet 容器允許使用其自身標準註冊 Filter
例項,但它不瞭解 Spring 定義的 Bean。您可以透過標準的 Servlet 容器機制註冊 DelegatingFilterProxy
,但將所有工作委託給一個實現了 Filter
介面的 Spring Bean。
下圖展示了 DelegatingFilterProxy
如何融入Filter
例項和 FilterChain
。

DelegatingFilterProxy
從 ApplicationContext
中查詢 Bean Filter0,然後呼叫 Bean Filter0。下面的列表展示了 DelegatingFilterProxy
的虛擬碼
DelegatingFilterProxy
虛擬碼-
Java
-
Kotlin
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
Filter delegate = getFilterBean(someBeanName); (1)
delegate.doFilter(request, response); (2)
}
fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val delegate: Filter = getFilterBean(someBeanName) (1)
delegate.doFilter(request, response) (2)
}
1 | 延遲獲取註冊為 Spring Bean 的 Filter。對於DelegatingFilterProxy 中的示例,delegate 是 Bean Filter0 的一個例項。 |
2 | 將工作委託給 Spring Bean。 |
DelegatingFilterProxy
的另一個好處是它允許延遲查詢 Filter
Bean 例項。這很重要,因為容器需要在啟動之前註冊 Filter
例項。然而,Spring 通常使用 ContextLoaderListener
來載入 Spring Bean,而這在 Filter
例項需要註冊之後才會完成。
FilterChainProxy
Spring Security 的 Servlet 支援包含在 FilterChainProxy
中。FilterChainProxy
是 Spring Security 提供的一個特殊 Filter
,它允許透過 SecurityFilterChain
委託給多個 Filter
例項。由於 FilterChainProxy
是一個 Bean,它通常被包裝在 DelegatingFilterProxy 中。
下圖展示了 FilterChainProxy
的作用。

SecurityFilterChain
SecurityFilterChain
被 FilterChainProxy 用來確定當前請求應該呼叫哪些 Spring Security Filter
例項。
下圖展示了 SecurityFilterChain
的作用。

SecurityFilterChain
中的安全 Filter 通常是 Bean,但它們註冊到 FilterChainProxy
而不是 DelegatingFilterProxy。相對於直接註冊到 Servlet 容器或 DelegatingFilterProxy,FilterChainProxy
提供了許多優勢。首先,它為 Spring Security 的所有 Servlet 支援提供了一個起點。因此,如果您嘗試對 Spring Security 的 Servlet 支援進行故障排除,在 FilterChainProxy
中新增一個除錯點是一個很好的開始位置。
其次,由於 FilterChainProxy
是 Spring Security 使用的核心,它可以執行一些非可選的任務。例如,它清除 SecurityContext
以避免記憶體洩漏。它還應用 Spring Security 的 HttpFirewall
來保護應用程式免受某些型別的攻擊。
此外,它在確定何時呼叫 SecurityFilterChain
方面提供了更大的靈活性。在 Servlet 容器中,Filter
例項僅根據 URL 進行呼叫。然而,FilterChainProxy
可以使用 RequestMatcher
介面根據 HttpServletRequest
中的任何內容來確定呼叫。
下圖展示了多個 SecurityFilterChain
例項

在多個 SecurityFilterChain 圖中,FilterChainProxy
決定應該使用哪個 SecurityFilterChain
。只有第一個匹配的 SecurityFilterChain
會被呼叫。如果請求的 URL 是 /api/messages/
,它首先匹配 SecurityFilterChain0
的模式 /api/**
,因此只調用 SecurityFilterChain0
,即使它也匹配 SecurityFilterChainn
。如果請求的 URL 是 /messages/
,它不匹配 SecurityFilterChain0
的模式 /api/**
,因此 FilterChainProxy
會繼續嘗試每個 SecurityFilterChain
。假設沒有其他 SecurityFilterChain
例項匹配,則會呼叫 SecurityFilterChainn
。
請注意,SecurityFilterChain0
只配置了三個安全 Filter
例項。然而,SecurityFilterChainn
配置了四個安全 Filter
例項。重要的是要認識到每個 SecurityFilterChain
都可以是唯一的,並且可以獨立配置。事實上,如果應用程式希望 Spring Security 忽略某些請求,SecurityFilterChain
可能包含零個安全 Filter
例項。
安全 Filter
安全 Filter 透過 SecurityFilterChain API 插入到 FilterChainProxy 中。這些 Filter 可用於多種不同目的,例如漏洞保護、認證、授權等。這些 Filter 以特定順序執行,以確保它們在正確的時間被呼叫,例如,執行認證的 Filter
應在執行授權的 Filter
之前被呼叫。通常不需要知道 Spring Security 的 Filter
順序。然而,有時瞭解順序會有幫助,如果您想了解它們,可以檢視 FilterOrderRegistration
程式碼。
這些安全 Filter 最常使用 HttpSecurity
例項進行宣告。為了說明上述段落,考慮以下安全配置:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults())
.formLogin(Customizer.withDefaults())
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
);
return http.build();
}
}
import org.springframework.security.config.web.servlet.invoke
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
csrf { }
httpBasic { }
formLogin { }
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
}
return http.build()
}
}
以上配置將導致以下 Filter
順序:
Filter | 新增者 |
---|---|
|
|
|
|
|
|
|
-
首先,呼叫
CsrfFilter
以防禦CSRF 攻擊。 -
其次,呼叫認證 Filter 對請求進行認證。
-
第三,呼叫
AuthorizationFilter
對請求進行授權。
可能還有上面未列出的其他 |
列印安全 Filter
通常,檢視特定請求呼叫的安全 Filter
列表很有用。例如,您想確保您新增的 Filter 位於安全 Filter 列表中。
Filter 列表會在應用程式啟動時以 DEBUG 級別列印,因此您可能會在控制檯輸出中看到類似以下內容:
2023-06-14T08:55:22.321-03:00 DEBUG 76975 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [ DisableEncodeUrlFilter, WebAsyncManagerIntegrationFilter, SecurityContextHolderFilter, HeaderWriterFilter, CsrfFilter, LogoutFilter, UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter, DefaultLogoutPageGeneratingFilter, BasicAuthenticationFilter, RequestCacheAwareFilter, SecurityContextHolderAwareRequestFilter, AnonymousAuthenticationFilter, ExceptionTranslationFilter, AuthorizationFilter]
這將很好地瞭解為每個 Filter 鏈配置的安全 Filter。
但這還不是全部,您還可以配置應用程式為每個請求列印每個獨立 Filter 的呼叫情況。這有助於檢視您新增的 Filter 是否被特定請求呼叫,或者檢查異常來自哪裡。要做到這一點,您可以配置應用程式記錄安全事件。
向 Filter 鏈新增 Filter
大多數情況下,預設的安全 Filter 足以為您的應用程式提供安全保障。然而,有時您可能希望向 SecurityFilterChain 新增自定義 Filter
。
HttpSecurity
提供了三種新增 Filter 的方法:
-
#addFilterBefore(Filter, Class<?>)
在另一個 Filter 之前新增您的 Filter -
#addFilterAfter(Filter, Class<?>)
在另一個 Filter 之後新增您的 Filter -
#addFilterAt(Filter, Class<?>)
用您的 Filter 替換另一個 Filter
新增自定義 Filter
如果您正在建立自己的 Filter,您需要確定它在 Filter 鏈中的位置。請檢視 Filter 鏈中發生的以下關鍵事件:
考慮您的 Filter 需要哪些事件已經發生才能確定其位置。以下是一條經驗法則:
如果您的 Filter 是 | 那麼將其放在之後 | 因為這些事件已經發生 |
---|---|---|
漏洞保護 Filter |
SecurityContextHolderFilter |
1 |
認證 Filter |
LogoutFilter |
1, 2 |
授權 Filter |
AnonymousAuthenticationFilter |
1, 2, 3 |
最常見的情況是應用程式新增自定義認證。這意味著它們應該放置在LogoutFilter 之後。 |
例如,假設您想新增一個 Filter,它獲取租戶 ID 頭部並檢查當前使用者是否具有訪問該租戶的許可權。
首先,我們建立 Filter
import java.io.IOException;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
public class TenantFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String tenantId = request.getHeader("X-Tenant-Id"); (1)
boolean hasAccess = isUserAllowed(tenantId); (2)
if (hasAccess) {
filterChain.doFilter(request, response); (3)
return;
}
throw new AccessDeniedException("Access denied"); (4)
}
}
上面的示例程式碼執行以下操作:
1 | 從請求頭部獲取租戶 ID。 |
2 | 檢查當前使用者是否具有訪問該租戶 ID 的許可權。 |
3 | 如果使用者具有許可權,則呼叫鏈中的其餘 Filter。 |
4 | 如果使用者沒有許可權,則丟擲 AccessDeniedException 。 |
除了實現 |
現在,您需要將 Filter 新增到 SecurityFilterChain 中。前面的描述已經提供了關於在哪裡新增 Filter 的線索,因為我們需要知道當前使用者,所以需要將其新增到認證 Filter 之後。
根據經驗法則,將其新增到鏈中最後一個認證 Filter AnonymousAuthenticationFilter
之後,如下所示:
-
Java
-
Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ...
.addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); (1)
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
// ...
.addFilterAfter(TenantFilter(), AnonymousAuthenticationFilter::class.java) (1)
return http.build()
}
1 | 使用 HttpSecurity#addFilterAfter 將 TenantFilter 新增到 AnonymousAuthenticationFilter 之後。 |
透過將 Filter 新增到 AnonymousAuthenticationFilter
之後,我們確保 TenantFilter
在認證 Filter 之後被呼叫。
就是這樣,現在 TenantFilter
將在 Filter 鏈中被呼叫,並檢查當前使用者是否具有訪問該租戶 ID 的許可權。
將您的 Filter 宣告為 Bean
當您將 Filter
宣告為 Spring Bean 時,無論是透過使用 @Component
註解還是在您的配置中將其宣告為 Bean,Spring Boot 都會自動將其註冊到嵌入式容器。這可能導致 Filter 被呼叫兩次,一次由容器呼叫,一次由 Spring Security 呼叫,並且順序不同。
因此,Filter 通常不是 Spring Bean。
但是,如果您的 Filter 需要成為 Spring Bean(例如,為了利用依賴注入),您可以透過宣告一個 FilterRegistrationBean
Bean 並將其 enabled
屬性設定為 false
來告訴 Spring Boot 不要將其註冊到容器中:
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
這使得 HttpSecurity
成為唯一新增它的地方。
自定義 Spring Security Filter
通常,您可以使用 Filter 的 DSL 方法來配置 Spring Security 的 Filter。例如,新增 BasicAuthenticationFilter
的最簡單方法是讓 DSL 完成此操作:
-
Java
-
Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(Customizer.withDefaults())
// ...
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
httpBasic { }
// ...
}
return http.build()
}
然而,如果您想自己構建一個 Spring Security Filter,您可以使用 addFilterAt
在 DSL 中指定它,如下所示:
-
Java
-
Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http
// ...
.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
注意,如果該 Filter 已經被新增,Spring Security 將丟擲異常。例如,呼叫 HttpSecurity#httpBasic
會為您新增一個 BasicAuthenticationFilter
。因此,以下安排會失敗,因為有兩個呼叫都嘗試新增 BasicAuthenticationFilter
:
-
Java
-
Kotlin
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
BasicAuthenticationFilter basic = new BasicAuthenticationFilter();
// ... configure
http
.httpBasic(Customizer.withDefaults())
// ... on no! BasicAuthenticationFilter is added twice!
.addFilterAt(basic, BasicAuthenticationFilter.class);
return http.build();
}
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val basic = BasicAuthenticationFilter()
// ... configure
http {
httpBasic { }
}
// ... on no! BasicAuthenticationFilter is added twice!
http.addFilterAt(basic, BasicAuthenticationFilter::class.java)
return http.build()
}
在這種情況下,由於您自己構建 BasicAuthenticationFilter
,請移除對 httpBasic
的呼叫。
如果您無法重新配置
|
處理安全異常
ExceptionTranslationFilter
允許將 AccessDeniedException
和 AuthenticationException
轉換為 HTTP 響應。
ExceptionTranslationFilter
作為安全 Filter 之一插入到 FilterChainProxy 中。
下圖展示了 ExceptionTranslationFilter
與其他元件的關係:

-
首先,
ExceptionTranslationFilter
呼叫FilterChain.doFilter(request, response)
來呼叫應用程式的其餘部分。 -
如果使用者未認證或發生
AuthenticationException
,則*開始認證*。-
HttpServletRequest
被儲存,以便在認證成功後用於重放原始請求。 -
使用
AuthenticationEntryPoint
從客戶端請求憑據。例如,它可能重定向到登入頁面或傳送WWW-Authenticate
頭部。
-
否則,如果發生
AccessDeniedException
,則*拒絕訪問*。呼叫AccessDeniedHandler
處理拒絕訪問。
如果應用程式沒有丟擲 |
ExceptionTranslationFilter
的虛擬碼看起來像這樣:
try {
filterChain.doFilter(request, response); (1)
} catch (AccessDeniedException | AuthenticationException ex) {
if (!authenticated || ex instanceof AuthenticationException) {
startAuthentication(); (2)
} else {
accessDenied(); (3)
}
}
1 | 如Filter 回顧中所述,呼叫 FilterChain.doFilter(request, response) 等同於呼叫應用程式的其餘部分。這意味著如果應用程式的另一部分(FilterSecurityInterceptor 或方法安全)丟擲 AuthenticationException 或 AccessDeniedException ,它會在這裡被捕獲和處理。 |
2 | 如果使用者未認證或發生 AuthenticationException ,則*開始認證*。 |
3 | 否則,*拒絕訪問*。 |
認證期間儲存請求
如處理安全異常所示,當請求未認證且需要認證的資源時,需要儲存針對已認證資源的請求,以便在認證成功後重新發送。在 Spring Security 中,這透過使用 RequestCache
實現儲存 HttpServletRequest
來完成。
RequestCache
HttpServletRequest
儲存在 RequestCache
中。當用戶成功認證後,RequestCache
被用來重放原始請求。RequestCacheAwareFilter
在使用者認證後使用 RequestCache
獲取儲存的 HttpServletRequest
,而 ExceptionTranslationFilter
在檢測到 AuthenticationException
後,在將使用者重定向到登入端點之前,使用 RequestCache
儲存 HttpServletRequest
。
預設情況下,使用 HttpSessionRequestCache
。下面的程式碼演示瞭如何自定義 RequestCache
實現,該實現僅在存在名為 continue
的引數時才檢查 HttpSession
中是否有儲存的請求。
continue
引數時,RequestCache
才檢查儲存的請求-
Java
-
Kotlin
-
XML
@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
requestCache.setMatchingRequestParameterName("continue");
http
// ...
.requestCache((cache) -> cache
.requestCache(requestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val httpRequestCache = HttpSessionRequestCache()
httpRequestCache.setMatchingRequestParameterName("continue")
http {
requestCache {
requestCache = httpRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="requestCache"/>
</http>
<b:bean id="requestCache" class="org.springframework.security.web.savedrequest.HttpSessionRequestCache"
p:matchingRequestParameterName="continue"/>
阻止請求被儲存
您可能出於多種原因不希望將使用者未認證的請求儲存在會話中。您可能希望將儲存解除安裝到使用者的瀏覽器上,或者將其儲存在資料庫中。或者您可能希望關閉此功能,因為您總是希望將使用者重定向到主頁,而不是他們登入前嘗試訪問的頁面。
為此,您可以使用 NullRequestCache 實現。
-
Java
-
Kotlin
-
XML
@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
RequestCache nullRequestCache = new NullRequestCache();
http
// ...
.requestCache((cache) -> cache
.requestCache(nullRequestCache)
);
return http.build();
}
@Bean
open fun springSecurity(http: HttpSecurity): SecurityFilterChain {
val nullRequestCache = NullRequestCache()
http {
requestCache {
requestCache = nullRequestCache
}
}
return http.build()
}
<http auto-config="true">
<!-- ... -->
<request-cache ref="nullRequestCache"/>
</http>
<b:bean id="nullRequestCache" class="org.springframework.security.web.savedrequest.NullRequestCache"/>
RequestCacheAwareFilter
RequestCacheAwareFilter
使用 RequestCache
重放原始請求。
日誌記錄
Spring Security 在 DEBUG 和 TRACE 級別提供所有安全相關事件的全面日誌記錄。當您除錯應用程式時,這非常有用,因為出於安全原因,Spring Security 不會在響應體中新增任何有關請求被拒絕的詳細資訊。如果您遇到 401 或 403 錯誤,您很可能會找到幫助您理解正在發生什麼的日誌訊息。
考慮一個示例,使用者嘗試對啟用了CSRF 保護的資源發出 POST
請求,但沒有 CSRF 令牌。如果沒有日誌,使用者將看到一個 403 錯誤,沒有任何解釋說明請求為何被拒絕。但是,如果您啟用 Spring Security 的日誌記錄,您將看到如下所示的日誌訊息:
2023-06-14T09:44:25.797-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Securing POST /hello
2023-06-14T09:44:25.797-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking DisableEncodeUrlFilter (1/15)
2023-06-14T09:44:25.798-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking WebAsyncManagerIntegrationFilter (2/15)
2023-06-14T09:44:25.800-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking SecurityContextHolderFilter (3/15)
2023-06-14T09:44:25.801-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking HeaderWriterFilter (4/15)
2023-06-14T09:44:25.802-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Invoking CsrfFilter (5/15)
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.security.web.csrf.CsrfFilter : Invalid CSRF token found for https://:8080/hello
2023-06-14T09:44:25.814-03:00 DEBUG 76975 --- [nio-8080-exec-1] o.s.s.w.access.AccessDeniedHandlerImpl : Responding with 403 status code
2023-06-14T09:44:25.814-03:00 TRACE 76975 --- [nio-8080-exec-1] o.s.s.w.header.writers.HstsHeaderWriter : Not injecting HSTS header since it did not match request to [Is Secure]
這清楚地表明 CSRF 令牌缺失,這就是請求被拒絕的原因。
要配置您的應用程式記錄所有安全事件,您可以將以下內容新增到您的應用程式中:
logging.level.org.springframework.security=TRACE
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- ... -->
</appender>
<!-- ... -->
<logger name="org.springframework.security" level="trace" additivity="false">
<appender-ref ref="Console" />
</logger>
</configuration>