Spring Security Servlet 概覽

原文在 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 雖然引入了一些其他概念,但整體流程差別不大:

image

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),比如 CorsFilterUsernamePasswordAuthenticationFilter 等。

上面提到的 RequestMatcher 是這個(gè)接口的默認(rèn)實(shí)現(xiàn)使用的。

綜上,我們可以得到一個(gè) big picture:

image

處理 Security Exception

這里說(shuō)的 Security Exception,其實(shí)只有兩種:AuthenticationExceptionAccessDeniedException。它們會(huì)在 ExceptionTranslationFilter 中被處理,而這個(gè) Filter 往往被安排在 SecurityFilterChain 的最后。

AuthenticationException

這個(gè)異常代表身份認(rèn)證失敗。ExceptionTranslationFilter 會(huì)調(diào)用 startAuthentication 方法處理它,其流程是:

  1. 清理 SecurityContextHolder 中的身份信息(后面的身份認(rèn)證內(nèi)容會(huì)涉及)
  2. 將當(dāng)前請(qǐng)求保存到 RequestCache 中,當(dāng)用戶(hù)通過(guò)身份驗(yàn)證后,會(huì)從其中取出當(dāng)前請(qǐng)求,繼續(xù)業(yè)務(wù)流程
  3. 調(diào)用 AuthenticationEntryPoint,要求用戶(hù)提供身份信息。方式可以是重定向到登陸頁(yè)面,也可以是返回?cái)y帶 WWW-Authenticate header 的 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ò) PrincipalCredentials 判斷用戶(hù)身份、通過(guò)調(diào)用 setAuthenticated 方法保存身份認(rèn)證是否通過(guò)的結(jié)果。

另外,在身份驗(yàn)證成功后,Authentication 中還保存了 GrantedAuthority 的集合,表示當(dāng)前用戶(hù)的角色和權(quán)限,用于后續(xù)的授權(quán)操作。

image

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)型的 AuthenticationProviderauthenticate 方法,直到身份驗(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 的功能。

整體流程如圖:

image

其中的 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)的 RESTful API,所以 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)包括 RoleVoterAuthenticationVoter。顧名思義,分別是根據(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)也是不允許。

image

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),AccessDecisionManagerAccessDecisionVoter 都沒(méi)有和 Servlet 綁定。

這么做的目的就是為了能支持 Method Security 和 AspectJ Security,這樣就能復(fù)用真正做授權(quán)邏輯的代碼。

我們可以看到 FilterSecurityInterceptor 擴(kuò)展了 AbstractSecurityInterceptor。而這個(gè)父類(lèi)的另外兩個(gè)實(shí)現(xiàn) MethodSecurityInterceptorAspectJMethodSecurityInterceptor 都是非 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
  • FilterSecurityInterceptor 過(guò)濾,使用先前創(chuàng)建的 Authentication 對(duì)象交給 AccessDecisionManager 授權(quán)
    • 最終成功調(diào)用業(yè)務(wù)方法
    • 或者拋出 AccessDeniedException
  • 上面拋出的 AuthenticationExceptionAccessDeniedException 將會(huì)被 ExceptionTranslationFilter 處理,轉(zhuǎn)化成 401 和 403 的響應(yīng)。
image

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過(guò)簡(jiǎn)信或評(píng)論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容