Spring Web Security是Java web開發領域的一個認證(Authentication)/授權(Authorisation)框架,基於Servlet技術,更確切的說是基於Servlet的Filter技術。因此,在學習Spring Web Security之前,有必要先對Servlet Filter的工作機制做個介紹。
注:Spring Security本身並不只是針對web的,但本文講的主要是其在web開發中的使用,因此下文提到Spring Security時主要指Spring Security在web開發中那一部分。
基本原理
Servlet技術是Java web開發的底層使能性技術,也就是說Java世界的很多web開發框架都是建立在Servlet基礎之上的,比如Structs和Spring MVC,前者的ActionServlet和后者的DispatcherServlet都只是標標准准的Servlet而已,並無什么特別之處。Servlet可以看成是處理web請求的基本單元,而Filter則是圍繞着Servlet,用於在web請求被處理之前或者之后對web請求(Request)和應答(Response)修改,其工作機制如下圖:
這里有三點比較重要:
- Filter即可以作用於Servlet之前、又可以作用於Servlet之后(Spring Security只用到了前者)。
- Filter在Request到達Servlet之前,可以直接將Response返回,此功能用於諸如在未登錄的情況下直接向用戶展示登錄頁面這樣的功能。
- 多個Filter起作用時有先后順序。
有了上面幾點,我們自己都可以實現一套認證授權機制出來。事實上,在Spring Security沒出來之前,很多開發者的確是基於Filter機制自己實現的一套安全框架。以下是一個Filter的例子:
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//1. 實現Filter邏輯,比如記錄訪問日志,認證,授權等
//2. 調用下一個Filter;
// 或者在沒有下一個Filter的情況下,調用請求終點站——Servlet
chain.doFilter(request,response);
//3. 實現Filter邏輯,到這里Servlet處理已經完成,
// 此處主要是對處理后的Response進行處理,比如記錄本次請求消耗的時間等
}
@Override
public void destroy() {
}
}
上文中提到,Filter是“圍繞”着Servlet的,這里的“圍繞”即是:在調用chain.doFilter(request,response);
之前和之后都可以執行Filter邏輯,只是一個在Request處理之前,一個在Request處理之后,Spring Security作為一個認證框架,只會作用在Request處理之前,這樣才能起到對Servlet的保護作用。
要使Spring Security生效,從可行性上來說,我們需要有一個Spring Security的Filter能夠被Servlet容器(比如Tomcat、Jetty等)感知到,這個Filter便是DelegatingFilterProxy,該Filter並不受Spring IoC容器的管理,在Servlet容器眼中,DelegatingFilterProxy只是一個Filter而已,跟其他的Servlet Filter沒什么卻別。
雖然DelegatingFilterProxy本身不在IoC容器中,它卻能夠訪問到IoC容器中的其他對象,這些對象才是真正完成Spring Security邏輯的對象。這些對象中的部分對象本身也實現了javax.servlet.Filter
接口,但是他們並不能被Servlet容器感知到,比如UsernamePasswordAuthenticationFilter。
DelegatingFilterProxy只是起代理作用,其本身也不是Spring Security的一部分,而是Spring Web中的一個基礎設施類。它將真正的邏輯代理給其他的被Spring IoC容器管理的對象(即Spring中的bean)。當它把邏輯代理給Spring Security bean時,便引入了Spring Security。我們完全可以使DelegatingFilterProxy將邏輯代理給自己編寫的Filter bean時,這樣的Filter做什么事情都可以,不見得是和Security相關的,也就是說DelegatingFilterProxy其實是一個很通用的代理類。
那么,問題來了?為什么不直接將Spring Security的Filter對象或者自己編寫的Filter對象直接對Servlet容器可見呢?是因為我們想讓這些Filter對象能夠享受到Spring IoC容器所帶來的好處。
那么,問題又來了?既然DelegatingFilterProxy是個通用的代理Filter,它是如何知道到底需要代理給哪個bean的呢?答案是:我們可以為DelegatingFilterProxy配置一個targetBeanName
字段,運行時DelegatingFilterProxy會去IoC容器中名字為該字段值的bean,並將邏輯代理給這個bean。比如,在使用Spring Security時,我們經常會在web.xml文件中做如下配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
DelegatingFilterProxy其實繼承了Spring中的GenericFilterBean,GenericFilterBean(本身實現了javax.servlet.Filter
接口)可以獲取到自身的名字——也就是上面代碼中的springSecurityFilterChain,DelegatingFilterProxy會將該名字賦值給targetBeanName字段,然后從IoC容器中找到名字為targetBeanName值(此時也即springSecurityFilterChain)的bean,並將邏輯代理給該bean。
也就是說,這個bean的名字必須為springSecurityFilterChain。開發者通常遇到Spring Security啟動時拋出異常的情況,很多時候其實就是因為所配置的DelegatingFilterProxy名字不為springSecurityFilterChain而導致的。
在Spring Security中,名字為springSecurityFilterChain的bean為一個FilterChainProxy類型的對象,該對象其實也只是起代理作用,但是跟DelegatingFilterProxy不同的是,它並不是一個Spring通用的代理對象,而是Spring Security自己的代理對象。FilterChainProxy維護了多個SecurityFilterChain,每一個SecurityFilterChain只會對特定的Request起作用(比如特定的URL),FilterChainProxy在接收到Request時,會從SecurityFilterChain列表中選出能夠處理該Request的那個SecurityFilterChain對象,然后將邏輯代理給該對象。這樣做的用處在於我們可以為不同的Request配置不同的認證方式,或者有些Request不用經過認證的(比如靜態資源)可以配置一個空的SecurityFilterChain,即該SecurityFilterChain里面沒有任何過濾邏輯。
這個SecurityFilterChain其實也不是最終完成Spring Security認證邏輯的對象,而是維護了多個Filter bean,這些Filter bean才是真正處理認證邏輯的對象。對於這些Filter bean來說,有的Filter用於處理用戶名+密碼登錄、有些用於生成登錄頁面、有些用於維持用戶登錄狀態、有的用於鑒權,根據具單一職責原則各司其職。
通常情況下,有些Filter bean在Spring Security中是必須的,Spring Security會自動為我們創建並配置這些bean,比如用於鑒權的FilterSecurityInterceptor,另外,我們也可以將自己的Filter bean將入Filter bean列表中,比如完成基於token的認證機制。另外,Filter bean的先后順序是重要的,你總不至於在用戶都還沒登錄就去做鑒權吧?
綜上,Spring Security的處理流程圖如下:
從上圖可以看到,不同的URL請求可能分配給不同的SecurityFilterChain,不同的SecurityFilterChain又可以包含不同的Filter列表,從而采用不同的認證方式。舉個例子,如果哪天我們需要開發一個系統既能提供無狀態的REST接口,又能提供傳統Spring MVC的頁面,那么可以為前者和后者分別創建各自的SecurityFilterChain,前者的SecurityFilterChain包含了處理Token認證的Filter Bean,后者的SecurityFilterChain包含基於Cookie/Session的Form表單登錄的Filter Bean。Spring Security已經為我們提供了一套足夠強大的認證設施,可以滿足我們大部分需求。但是,Spring Security默認提供的認證設施主要針對傳統的基於Cookie和Session的的Web應用,對於基於Token的認證方式需要我們自己開發Filter Bean。
關鍵Filter
ChannelProcessingFilter
用於將HTTP請求重定向到HTTPS頁面,比如登錄頁面需要用戶輸入密碼,這種帶有敏感信息的頁面通常需要通過HTTPS保護,如果用戶訪問的是HTTP,那么ChannelProcessingFilter可以自動重定向到HTTPS的登錄頁面。
SecurityContextPersistenceFilter
保存用戶的登錄狀態,作用為:用戶登錄之后,以后的訪問就不需要再登錄了。默認情況下登錄狀態保存在HTTP Session里面。這對於傳統的Web應用來說是比較常見的,但是對於某些要求無狀態的應用來說,便不合適了。
UsernamePasswordAuthenticationFilter
用於處理基於Form登錄的認證,認證成功重定向到指定頁面,認證失敗向用戶重新返回登錄界面並提示錯誤。UsernamePasswordAuthenticationFilter繼承自AbstractAuthenticationProcessingFilter,AbstractAuthenticationProcessingFilter可以配置一個AuthenticationSuccessHandler和一個AuthenticationFailureHandler,認證成功之后將調用AuthenticationSuccessHandler,比如像UsernamePasswordAuthenticationFilter一樣重定向到某個頁面,也可以根據自定義向用戶返回一個JWT的Token;認證失敗(比如用戶名或密碼不正確)后將調用AuthenticationFailureHandler,比如像UsernamePasswordAuthenticationFilter一樣重新返回登錄頁面,也可以根據自定義向用戶返回一個401狀態碼。
AbstractAuthenticationProcessingFilter並不完成認證邏輯,而是將其交給AuthenticationManager,AuthenticationManager進而代理給AuthenticationProvider,AuthenticationProvider驗證用戶提供的憑證是否正確(比如從數據庫加載用戶的密碼然后與用戶提供的密碼對比,或者與LDAP服務器通信驗證用戶名和密碼)。
ExceptionTranslationFilter和FilterSecurityInterceptor
這兩個Filter通常是結合在一起用的,前者負責處理后者所拋出的異常並做相應的處理,后者主要用於鑒權。ExceptionTranslationFilter在處理異常時,如果異常為AuthenticationException類型,表示用戶認證都失敗了(比如還沒有經過認證),此時將調用AuthenticationEntryPoint開啟認證過程,比如向用戶展示登錄頁面;如果異常為AccessDeniedException,表示用戶可能已經登錄但是沒有足夠的權限,此時將調用AccessDeniedHandler,比如向用戶展示“你沒有權限”的通知頁面。
更多關於Spring Security架構方面的知識,請參考這篇Spring官網文章。