上一篇主要介紹了Spring Secuirty中的過濾器鏈SecurityFilterChain是如何配置的,那麼在配置完成之後,SecurityFilterChain是如何在應用程序中調用各個Filter,從而起到安全防護的作用,本文主要圍繞SecurityFilterChain的工作原理做詳細的介紹。
一、Filter背景知識
因為Spring Security底層依賴Servlet的過濾器技術,所以先簡單地回顧一下相關背景知識。
過濾器Filter是Servlet的標準組件,自Servlet 2.3版本引入,主要作用是在Servlet實例接受到請求之前,以及返回響應之後,這兩個方向上進行動態攔截,這樣就可以與Servlet主業務邏輯解耦,從而實現靈活性和可擴展性,利用這個特性可以實現很多功能,例如身份認證,統一編碼,數據加密解密,審計日誌等等。
Filter接口定義了3個方法:doFilter,init和destory,其中doFilter就是請求進入過濾器時需要執行的邏輯,偽代碼實現如下
public class ExampleFilter implements Filter {
…
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
doSomething();
chain.doFilter(request,response);
}
…
}
其中FilterChain中維護了一個所有已註冊的過濾器數組,它組成了真正的“過濾器鏈”,下面是FilterChain的實現類ApplicationFilterChain的部分源碼:當請求到達Servlet容器時,就會創建出一個FilterChain實例,然後調用FilterChain#doFilter方法,這時會從數組中取出下一個過濾器,並調用Filter#doFilter方法,在方法末尾又會將請求繼續交由FilterChain處理,如此往復,從而實現職責鏈模式的調用方式。
private void internalDoFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
ApplicationFilterConfig filterConfig = filters[pos++];
try {
Filter filter = filterConfig.getFilter();
...
if (Globals.IS_SECURITY_ENABLED) {
// ...
} else {
filter.doFilter(request, response, this);
}
} catch (...) {
...
}
return;
}
// We fell off the end of the chain -- call the servlet instance
try {
...
// Use potentially wrapped request from this point
if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) &&
Globals.IS_SECURITY_ENABLED) {
...
} else {
servlet.service(request, response);
}
} catch (...) {
...
} finally {
...
}
}
Filter實例可以在web.xml中註冊,同時設置URL映射邏輯,當URL符合設置的規則時,便會進入該Filter,舉個例子,在Spring Boot問世之前開發一個普通的Spring MVC應用時,經常會配置一個CharacterEncodingFilter,用於統一請求和響應的編碼,以避免一些中文亂碼的問題
<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern> <!-- 相當於攔截所有請求 -->
</filter-mapping>
二、SecurityFilterChain的必要性
再回到SecurityFilterChain,先來思考一個問題:基於上面所介紹的Filter,我們自然會想到,定義一系列與安全相關的Filter,例如我們在上一篇提到的那些包括認證,鑑權等在內的Filter,然後只要把他們一個個註冊到FilterChain中,就可以實現各種安全特性,看起來也並不需要Spring Security提供的SecuriyFilterChain,也正因如此,初學者經常會有一個疑問,就是明明加一個Filter就可以解決的事,為什麼搞得這麼複雜?
那麼SecurityFilterChain的必要性是什麼?我們一層一層逐步説明這個問題:
- 首先要解決的是如何在Filter中獲取Spring容器中Bean對象,因為在Servlet容器中啓動時,各個Filter的實例便會初始化並完成註冊,此時Spring Bean對象還沒有完成整個加載過程,不能直接注入,不過很容易想到,可以用一個“虛擬”的Filter在Servlet容器啓動時先完成註冊,然後在執行doFilter時,再獲取對應的Spring Bean作為實際的Filter實例,執行具體的doFilter邏輯,這是一個典型的委派模式,Spring Security為此提供了一個名為DelegatingFilterProxy的類,下文再作詳細介紹。
- 解決了Spring Bean容器與Servlet Filter整合的問題之後,我們是否可以將每一個Filter都通過DelegatingFilterProxy的模式添加到FilterChain中?試想一下,如果每個Spring Security的Filter都分別創建一個獨立的委派類,那麼通過ApplicationContext查找bean的代碼就會反覆出現,這在很大程度上違背了依賴注入的原則,也極大了增加了維護成本和開發成本,為了解決這個問題,在上述DelegateFilterProxy基礎上,Spring Security又引入了一個代理類FilterChainProxy,它可以看作是Spring Security Filter的統一入口,此時,從Servlet的FIlterChain角度來看,整個Spring Security只定義了一個Filter,即DelegatingFilterProxy,而執行doFilter時則委派給了FilterChainProxy,這樣就可以利用這個入口簡化很多工作,例如官方文檔中提到,可以在調試Spring Security功能時,將斷點設置在這個入口,方便我們跟蹤定位問題等等
- FilterChainProxy作為統一收口,同時也起到了打通SecurityFilterChain的橋樑作用,在調用doFilter方法時,實際上都交給某個SecurityFilterChain實例執行,到這裏請求才算是進入了我們使用HttpSecurity配置的各個Filter,而在執行SecurityFilterChain的前後位置,又可以統一添加一些處理,例如添加Spring Security的防火牆HttpFirewall,用以防範某些特定類型的攻擊
- 最後還有一點,Servlet Filter本身也存在一定的侷限性,例如映射配置不夠靈活,只能根據URL進行匹配,而SecurityFilterChain通過RequestMatcher接口實現了不同匹配邏輯及組合,大大豐富了匹配規則映射的能力
綜上所述,通過DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain這樣的三層結構關係,使得SecurityFilterChain中的各個Filter被當成了一個整體,置於Servlet FilterChain之中,又能和其他的Filter獨立開,不論我們如何配置SecurityFilterChain,都不會引起Servlet FilterChain的變更,這樣的設計很好地遵循了開放封閉原則,即對Servlet Filter的修改是保持封閉的,而對Spring Security Filter的配置和擴展是保持開放的。
其實,我們在很多Spring的框架中,都可以見到這種設計,本質上來説,即通過添加一箇中間層來達到解耦的目的,我們應該深入地理解這種設計,並學以致用。
三、SecuriyFilterChain的工作原理
討論完SecurityFilterChain必要性,再來介紹SecurityFilterChain的工作原理就會變得比較好理解了
1. 註冊DelegatingFilterProxy
在非Spring Boot環境可以通過web.xml進行註冊,配置如下:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
而在Spring Boot環境下,則是通過RegistrationBean的方式註冊Servlet組件,具體實現類為DelegatingFilterProxyRegistrationBean,它由SecurityFilterAutoConfiguration配置類創建出來,並在Servlet容器啓動的時候完成Filter的註冊。
完成註冊後,當Servlet容器啓動時,FilterChain就包含了DelegatingFilterProxy這個Filter。
2.委派FilterChainProxy
上文提到在執行DelegatingFilterProxy的doFilter方法時,實際上都是交給FilterChainProxy來執行,它是由Spring容器託管的bean對象,通過下面WebSecurityConfiguration配置類源碼可以看到,其中定義了一個名稱為“springSecurityFilterChain”的Bean,而其中webSecurity#build方法返回的就是FilerChainProxy的實例,其構建過程和上一篇介紹的HttpSecurity類似,這裏就不再展開。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) // "springSecurityFilterChain"
public Filter springSecurityFilterChain() throws Exception {
...
return this.webSecurity.build();
}
委派過程比較簡單,下面是DelegatingFilterProxy#doFilter方法的源碼(可以忽略併發控制的代碼),當請求進入doFilter之後,首先調用initDelegate方法,這裏利用Spring的ApplicationContext#getBean方法獲取名為“springSecurityFilterChain“的bean對象,即FilterChainProxy,然後調用其doFilter方法,這樣就完成了委派調用。
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = getTargetBeanName(); // "springSecurityFilterChain"
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = wac.getBean(targetBeanName, Filter.class);
if (isTargetFilterLifecycle()) {
delegate.init(getFilterConfig());
}
return delegate;
}
protected void invokeDelegate(
Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
delegate.doFilter(request, response, filterChain);
}
3. 執行SecurityFilterChain的過濾器鏈
嚴格來説,最終執行doFilter的並不是SecuritFilterChain,FilterChainProxy內部維護了一個SecurityFilterChain的List列表,在調用doFilter方法時,會根據SecurityFilterChain#match方法匹配的結果決定選擇某一個SecurityFilterChain,然後取出該SecurityFilterChain所有的Filter,用其構造一個VirtualFilterChain,這才是實際意義上過濾器鏈執行的入口。
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(firewallRequest); // 重點關注這個方法,獲取到某個SecurityFilterChain的所有Filter
if (filters == null || filters.size() == 0) {
...
firewallRequest.reset();
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
return;
}
...
FilterChain reset = (req, res) -> {
...
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
// 裝飾器模式,實際上返回了VirtualFilterChain的實例
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
for (SecurityFilterChain chain : this.filterChains) {
...
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
public FilterChain decorate(FilterChain original, List<Filter> filters) {
return new VirtualFilterChain(original, filters);
}
VirtualFilterChain的實現也並不複雜,其doFilter方法源碼如下,原理和Servlet的FilterChain的實現類ApplicationFilterChain基本類似,不過當所有Filter都執行完之後,它會交給originalChain繼續執行,即回到Servlet的FilterChain。上文提到,如果要打斷點debug,這裏是一個比較好的位置,可以看到Spring Security中定義各個Filter執行的過程。
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.currentPosition == this.size) {
this.originalChain.doFilter(request, response);
return;
}
this.currentPosition++;
Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1);
if (logger.isTraceEnabled()) {
String name = nextFilter.getClass().getSimpleName();
logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size));
}
nextFilter.doFilter(request, response, this);
}
最後,再結合Spring Security官方文檔的圖示,可以更好地理解整個執行流程