原文在 GitHub Pages 上,內(nèi)容無(wú)差別
Spring Security 是 Spring 框架中用于實(shí)現(xiàn) Security 相關(guān)需求的項(xiàng)目。我們可以通過(guò)使用這個(gè)框架來(lái)實(shí)現(xiàn)項(xiàng)目中的安全需求。
今天這篇文章將會(huì)討論 Spring Security Servlet 是如何工作的。
之所以將內(nèi)容限定到 Servlet,是因?yàn)楝F(xiàn)在 Spring Security 已經(jīng)開(kāi)始支持 Reactive Web Server,因?yàn)榈讓拥募夹g(shù)不同,當(dāng)然需要分開(kāi)討論。
Spring Security 在哪里生效
我們知道,在 Servlet 中,一次請(qǐng)求會(huì)經(jīng)過(guò)這樣的階段: client -> servlet container -> filter -> servlet
而 Spring MVC 雖然引入了一些其他概念,但整體流程差別不大:

Spring Security 則是通過(guò)實(shí)現(xiàn)了 Filter 來(lái)實(shí)現(xiàn)的 Security 功能。這樣一來(lái),只要使用了 Servlet Container,就可以使用 Spring Security,不需要關(guān)心有沒(méi)有使用 Spring Web 或別的 Spring 項(xiàng)目。
DelegatingFilterProxy
這是 Spring Security 實(shí)現(xiàn)的一個(gè) Servlet Filter。它被加入到 Servlet Filter Chain 中,將 filter 的任務(wù)橋接給 Spring Context 管理的 bean。
FilterChainProxy
這是被 DelegatingFilterProxy 封裝的一個(gè) Filter,其實(shí)也是一個(gè)代理。這個(gè)類(lèi)維護(hù)了一個(gè) List<SecurityFilterChain>,它會(huì)將請(qǐng)求代理給這個(gè) list 進(jìn)行 filter 的工作。
但這個(gè)代理不是遍歷整個(gè) list,而是通過(guò) RequestMatcher 來(lái)判斷是否要使用這一個(gè) SecurityFilterChain。我們配置時(shí)寫(xiě)的 mvcMatchers 之類(lèi)的方法就會(huì)影響到這里的判斷。
SecurityFilterChain
這個(gè)接口的實(shí)現(xiàn)維護(hù)了一個(gè) Filter 列表,這些 Filter 是真正進(jìn)行 filter 工作的類(lèi),比如 CorsFilter、UsernamePasswordAuthenticationFilter 等。
上面提到的 RequestMatcher 是這個(gè)接口的默認(rèn)實(shí)現(xiàn)使用的。
綜上,我們可以得到一個(gè) big picture:

處理 Security Exception
這里說(shuō)的 Security Exception,其實(shí)只有兩種:AuthenticationException 和 AccessDeniedException。它們會(huì)在 ExceptionTranslationFilter 中被處理,而這個(gè) Filter 往往被安排在 SecurityFilterChain 的最后。
AuthenticationException
這個(gè)異常代表身份認(rèn)證失敗。ExceptionTranslationFilter 會(huì)調(diào)用 startAuthentication 方法處理它,其流程是:
- 清理
SecurityContextHolder中的身份信息(后面的身份認(rèn)證內(nèi)容會(huì)涉及) - 將當(dāng)前請(qǐng)求保存到
RequestCache中,當(dāng)用戶(hù)通過(guò)身份驗(yàn)證后,會(huì)從其中取出當(dāng)前請(qǐng)求,繼續(xù)業(yè)務(wù)流程 - 調(diào)用
AuthenticationEntryPoint,要求用戶(hù)提供身份信息。方式可以是重定向到登陸頁(yè)面,也可以是返回?cái)y帶WWW-Authenticateheader 的 HTTP 響應(yīng)
AccessDeniedException
這個(gè)異常代表授權(quán)失敗,意味著當(dāng)前用戶(hù)的身份已確認(rèn),但被服務(wù)拒絕了請(qǐng)求。
ExceptionTranslationFilter 會(huì)將這個(gè)異常交給 AccessDeniedHanlder 處理。默認(rèn)的實(shí)現(xiàn)會(huì)重定向到 /error,并得到一個(gè) 403 響應(yīng)。
了解了 Spring Security 在哪里生效之后,我們?cè)賮?lái)看看兩個(gè)重要的問(wèn)題:身份認(rèn)證和授權(quán)。
身份認(rèn)證
SecurityContextHolder
SecurityContextHolder 是保存身份信息的地方,默認(rèn)通過(guò) ThreadLocal 的方式保存 SecurityContext??梢酝ㄟ^(guò)靜態(tài)方法 SecurityContextHolder.getSecurityContext() 獲取當(dāng)前線(xiàn)程的 SecurityContext。
SecurityContextHolder.getSecurityContext()方法雖然是靜態(tài)的,可以在任何地方調(diào)用。但個(gè)人不建議這么做,而是應(yīng)該作為參數(shù)傳遞給使用到的方法,避免當(dāng)前的SecurityContext成為隱式輸入。
SecurityContext 是一個(gè)接口,提供 getAuthentication 方法獲取當(dāng)前用戶(hù)信息;setAuthentication 設(shè)置當(dāng)前用戶(hù)信息。
Authentication 也是一個(gè)接口,它的實(shí)現(xiàn)保存了當(dāng)前用戶(hù)的信息。在身份驗(yàn)證的流程中,總是在圍繞著 Authentication 操作 —— 通過(guò) Principal 和 Credentials 判斷用戶(hù)身份、通過(guò)調(diào)用 setAuthenticated 方法保存身份認(rèn)證是否通過(guò)的結(jié)果。
另外,在身份驗(yàn)證成功后,Authentication 中還保存了 GrantedAuthority 的集合,表示當(dāng)前用戶(hù)的角色和權(quán)限,用于后續(xù)的授權(quán)操作。

AuthenticationManager
AuthenticationManager 提供了 authenticate() 方法用于進(jìn)行身份驗(yàn)證,但并不是它自己完成,而是通過(guò) AuthenticationProvider 完成。
AuthenticationProvider 提供 support(Authentication) 方法用于判斷是否能夠驗(yàn)證這種類(lèi)型的 Authentication。
在 AuthenticationManager 的實(shí)現(xiàn) ProviderManager 中保存了 List<AuthenticationProvider>。它會(huì)按順序調(diào)用支持當(dāng)前 Authentication 類(lèi)型的 AuthenticationProvider 的 authenticate 方法,直到身份驗(yàn)證成功(返回值 non-null)或全部失敗。
在這個(gè)過(guò)程中出現(xiàn)的 AuthenticationException 將會(huì)被上面提到的 ExceptionTranslationFilter 處理。
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter.doFilter() 方法實(shí)現(xiàn)了身份驗(yàn)證的流程,包括成功和失敗的處理。
它提供了一個(gè)抽象方法 attemptAuthentication() 用于身份驗(yàn)證。子類(lèi)可以調(diào)用它的 authenticationManager 來(lái)實(shí)現(xiàn) authenticate 的功能。
整體流程如圖:

其中的 1 & 2 都在 attemptAuthentication() 方法中完成,需要子類(lèi)實(shí)現(xiàn)。
3 通過(guò) successfulAuthentication() 方法實(shí)現(xiàn),可以被子類(lèi)重寫(xiě)。
4 中除 SessionAuthenticationStrategy 外都交給 unsuccessfulAuthentication() 方法處理,同樣可以被子類(lèi)重寫(xiě)。
考慮到越來(lái)越多的應(yīng)用都是基于無(wú)狀態(tài)的
RESTfulAPI,所以SessionAuthenticationStrategy不會(huì)在本文涉及
授權(quán)
在 Servlet 中授權(quán)
Spring Security 授權(quán)的入口有很多處,關(guān)注到 Servlet 上的話(huà),那就是 FilterSecurityInterceptor 這個(gè) Filter。他會(huì)被配置到所有的 AbstractAuthenticationProcessingFilter 子類(lèi)之后,這樣他就能從 SecurityContextHodler 中得到 Authentication,用以進(jìn)行授權(quán)。
AccessDecisionManager
授權(quán)的過(guò)程,被交給 AccessDecisionManager 實(shí)現(xiàn),他的 decide 方法接收三個(gè)參數(shù):
-
Authentication:這就是從SecurityContextHolder中拿到的對(duì)象 - secureObject:這是一個(gè) Object 類(lèi)型,對(duì)于
FilterSecurityIntercepter來(lái)說(shuō),會(huì)用 request、response 和 filterChain 創(chuàng)建一個(gè)FilterInvocation對(duì)象作為 secureObject -
Collection<ConfigAttribute>:FilterSecurityIntercepter使用ExpressionBasedFilterInvocationSecurityMetadataSource保存這些ConfigAttribute,這些值用來(lái)給AccessDecisionManager提供做判斷的信息
AccessDecisionManager 自然也不是包含具體的判斷邏輯的角色,真正根據(jù)上面三個(gè)參數(shù)來(lái)授權(quán)的類(lèi),其實(shí)是 AccessDecisionVoter。
AccessDecisionVoter
AccessDecisionVoter 提供一個(gè) vote 方法,接收上面的 decide 方法一樣的參數(shù)。
他的實(shí)現(xiàn)包括 RoleVoter 和 AuthenticationVoter。顧名思義,分別是根據(jù)角色和權(quán)限信息來(lái)判斷是否授權(quán)的實(shí)現(xiàn)。而什么樣的角色/權(quán)限可以訪(fǎng)問(wèn)這個(gè)對(duì)象則是通過(guò) ConfigAttribute 傳入的。
不管具體的 Voter 實(shí)現(xiàn)如何,最終會(huì)返回一個(gè) int,只有 -1、0、1 三個(gè)值,分別表示拒絕、棄權(quán)、同意。
一個(gè) AccessDecisionManager 會(huì)管理多個(gè) AccessDecisionVoter,最終會(huì)根據(jù)所有 voter 的結(jié)果來(lái)判斷是授權(quán)成功,還是拋出 AccessDeniedException。
具體判斷的策略則是交給了 AccessDecisionManager 的三個(gè)實(shí)現(xiàn)來(lái)決定:
ConsensusBased
像一般的比賽投票一樣,票多的結(jié)果就是最終決定。
可以配置票數(shù)相等(不是全部棄權(quán))時(shí),結(jié)果是否通過(guò),默認(rèn)值是允許通過(guò)。
也可以配置全部棄權(quán)時(shí),結(jié)果是否通過(guò),默認(rèn)值是不允許。
AffirmativeBased
只要有一個(gè) voter 同意,就允許通過(guò)。
同樣可以配置全部棄權(quán)時(shí)的決定,默認(rèn)也是不允許。
UnanimousBased
要求所有 voter 一致同意時(shí)才通過(guò)。
同樣可以配置全部棄權(quán)時(shí)的決定,默認(rèn)也是不允許。

AbstractSecurityInterceptor
到此,授權(quán)用到的核心類(lèi)基本介紹完了,讓我們回過(guò)頭來(lái)想一個(gè)問(wèn)題:FilterSecurityInterceptor 明明是一個(gè) Filter,為什么要叫做 Interceptor?
如果回顧上面介紹的這些類(lèi),你會(huì)發(fā)現(xiàn)只有 FilterSecurityInterceptor 通過(guò)實(shí)現(xiàn) Filter 接口和 Servlet 綁定了起來(lái),AccessDecisionManager 和 AccessDecisionVoter 都沒(méi)有和 Servlet 綁定。
這么做的目的就是為了能支持 Method Security 和 AspectJ Security,這樣就能復(fù)用真正做授權(quán)邏輯的代碼。
我們可以看到 FilterSecurityInterceptor 擴(kuò)展了 AbstractSecurityInterceptor。而這個(gè)父類(lèi)的另外兩個(gè)實(shí)現(xiàn) MethodSecurityInterceptor 和 AspectJMethodSecurityInterceptor 都是非 Servlet 的實(shí)現(xiàn)。由此便做到了對(duì)不同的授權(quán)方式的支持,并且復(fù)用了代碼。
關(guān)于授權(quán),還有一個(gè)很重要的 ACL 沒(méi)有提到,它并沒(méi)有影響整個(gè)授權(quán)的架構(gòu),這里就不寫(xiě)了,以后有空再說(shuō)吧。
總結(jié)
這篇文章梳理了 Spring Security 在 Servlet 中的代碼架構(gòu),構(gòu)建了一個(gè) big picture。
通過(guò)這篇文章,我們了解到,在請(qǐng)求到達(dá)真正處理業(yè)務(wù)的 Controller 之前,經(jīng)歷了:
- 各種
AbstractAuthenticationProcessingFilter過(guò)濾請(qǐng)求,交給AuthenticationManager管理的AuthenticationProvider嘗試不同的身份認(rèn)證方式- 最終得到一個(gè)保存在
SecurityContextHolder中的Authentication對(duì)象 - 或者無(wú)法確定身份的情況下拋出
AuthenticationException
- 最終得到一個(gè)保存在
- 被
FilterSecurityInterceptor過(guò)濾,使用先前創(chuàng)建的Authentication對(duì)象交給AccessDecisionManager授權(quán)- 最終成功調(diào)用業(yè)務(wù)方法
- 或者拋出
AccessDeniedException
- 上面拋出的
AuthenticationException和AccessDeniedException將會(huì)被ExceptionTranslationFilter處理,轉(zhuǎn)化成 401 和 403 的響應(yīng)。

有了這個(gè) big picture,在接下來(lái)研究細(xì)節(jié)的時(shí)候,就不至于摸不著頭腦了。