亂講SpringSecurity
最近又想起了SpringSecurity,記着頭兩年這東西把我難壞了,當然現在我對他也玩的不那麼好看,先記一下最近學習的結果,如果以後還有機會學習,再擼一次
SpringSecurity的工作流程
先說明幾個名詞
- 身份認證,指的是確認你的身份或者說就是登陸信息
- 權限認證,指的是判斷你有沒有訪問這個資源的權限
- 身份認證流程,指的是獲得身份認證的過程,這可能需要幾個Filter和瀏覽器的跳轉完成的
以他默認的基於Session的方式說明
綠色部分是用於加載已經存在於本次會話的 SecurityContex
藍色部分是用於身份認證流程的,也就是沒有認證的情況下會進入這部分在
橙色部分是權限認定的部分,會最終決定是權限足夠還是未登陸。
以上的功能都是Security的過濾器鏈來完成的,特別說明一下,分析Security流程是不要只抓住一次請求去分析,要結合來回幾次分析,同時一定要分析好,這次請求是資源訪問還是身份認證請求
SpringSecurity的主要Filter
Filter | 本文重點 |
---|---|
ChannelProcessingFilter | |
ConcurrentSessionFilter | |
WebAsyncManagerIntegrationFilter | |
SecurityContextPersistenceFilter | yes |
HeaderWriterFilter | |
CorsFilter | |
CsrfFilter | |
LogoutFilter | |
OAuth2AuthorizationRequestRedirectFilter | |
Saml2WebSsoAuthenticationRequestFilter | |
X509AuthenticationFilter | |
AbstractPreAuthenticatedProcessingFilter | |
CasAuthenticationFilter | |
OAuth2LoginAuthenticationFilter | |
Saml2WebSsoAuthenticationFilter | |
UsernamePasswordAuthenticationFilter | yes |
ConcurrentSessionFilter | |
OpenIDAuthenticationFilter | |
DefaultLoginPageGeneratingFilter | yes |
DefaultLogoutPageGeneratingFilter | |
DigestAuthenticationFilter | |
BearerTokenAuthenticationFilter | |
BasicAuthenticationFilter | |
RequestCacheAwareFilter | |
SecurityContextHolderAwareRequestFilter | |
JaasApiIntegrationFilter | |
RememberMeAuthenticationFilter | |
AnonymousAuthenticationFilter | yes |
OAuth2AuthorizationCodeGrantFilter | |
SessionManagementFilter | |
ExceptionTranslationFilter | yes |
FilterSecurityInterceptor | yes |
Security的所有邏輯都在上面的這個鏈中實現的,本文中主要說明的幾個Filter,這些個Filter可以完成一次完整的,基於Session的password認證與鑑權的流程。其他的Filter,有些與這些功能上相似,但都是結合不同的認證邏輯的不同實現,有些我也我也不知道幹什麼的(T_T),這些有的不在默認的依賴中,需要使用的話還需要引入特定的依賴,比如OAuth2的相關Filter。所以我們先關心下面這幾個Filter就可以了
Filter |
---|
SecurityContextPersistenceFilter |
UsernamePasswordAuthenticationFilter |
DefaultLoginPageGeneratingFilter |
AnonymousAuthenticationFilter |
ExceptionTranslationFilter |
FilterSecurityInterceptor |
SpringSecurity主要的對象
用於保存權限與身份數據的對象
- GrantedAuthority 保存當前請求持有的權限,比如 ROLE_ADMIN,ROLE_MANAGER,最終進行權限認證的時候會比對這個請求的權限和請求想要訪問資源所需要的權限
- Authentication 用於保存認證信息,以最常見的登陸用戶舉例,這個對象會持有用戶的用戶名(Principal),密碼(Credentials),用戶擁有的權限(Authorities/ GrantedAuthority),其他詳細信息和是否認證過的標識
- SecurityContext 保存Authentication ,他在代碼上的意思不光是用於保存Authentication數據,他可以讓你更改保存Authentication的方式
- SecurityContextHolder 保存 SecurityContext,這個類提供的功能是可以提供不同的SecurityContext的共享策略,對於普通的WEB工程,他會將SecurityContext保存在一個ThreadLocal中
圖怎麼這麼大
用於認證身份信息的對象
- AuthenticationManager 用於通過一個不完整的Authentication 計算出一個數據完整的身份認證過的 Authentication,就是說,入參可能只有username,但是出參會包含username, GrantedAuthority ,details,isAuthenticated等等更多的信息,一個默認的,使用最多的例子是ProviderManager
- AuthenticationProvider 這個類用於具體計算Authentication,通常來看,他是AuthentiationManager持有的對象,AuthenticationManager通過這個對象來得到具體的Authentication,但根據你的認證方式不同,也可以沒有這個對象,比如使用OAuth2時 OAuth2AuthenticationManager。
認證失敗的處理類
- AuthenticationFailureHandler 如果你的身份認證流程是在SecurityFilterChain中的,比如你使用UsernamePasswordAuthenticationFilter,那麼在這個過程中,獲得認證信息時(比如去數據庫檢索用戶)失敗了,會使用這個類去處理,一般會重定向到一個登陸錯誤頁面(比如默認的 /login?error)或者響應 401 (參考SimpleUrlAuthenticationFailureHandler)
- AuthenticationEntryPoint 這個是在最終認證權限時如果發現請求沒有身份認證 ,那麼會使用這個類進行處理,處理的方式可能和AuthenticationFailureHandler類似,但是響應給前臺的信息可能會不同,因爲這個類代表着沒有登陸,AuthenticationFailureHandler代表登陸過程出錯,比如密碼錯誤,這時前臺的良好的表現是會提示用戶這個錯誤
- AccessDeniedHandler 這個是在最終認證權限時發現請求的權限不對,比如希望有ROLE_ADMIN,而你是ROLE_NORMAL,那麼會響應一個 403
保存、獲得SecurityContext的類
SecurityContextRepository 這個類一般沒有人關注,網上一些例子也沒有使用這個類,而是編寫其他Filter類來實現其功能,他的功能是加載SecurityContext, 他工作在SecurityContextPersistenceFilter 中,默認的,基於Session的工程,這個類的功能就是在Session中獲得SecurityContext。網上有一些教程使用jwt寫成鑑權的,不希望在Session中保存SecurityContext ,而是通過token 獲得,那麼他們會寫一個Filter中Request header中獲得token ,然後加載一個SecurityContext,這個Filter的位置放在 SecurityContextPersistenceFilter後面。其實spring security 是提供了這種擴展的,就是重新實現一個SecurityContextRepository,在配置Security時 這樣配置
http.securityContext().securityContextRepository(new BarnSecurityContextRepository()).and()
SpringSecurity具體流程
我們只以最普通的以Session爲基礎的,數據庫鑑權的方式,相關的Filter可以有下面這幾個
其大概流程是這樣子的
這UML畫的不標準,可能只有我能看明白吧(T_T),看下面的吧。
一個請求進入可能分爲3種情況
- POST /login
- GET /login,GET /login?error
- 其他
第一種情況,他會他會走到 UsernamePasswordAuthenticationFilter
,去加載認證信息和對應的權限,如果成功,跳轉到到登陸成功頁面,如果失敗會將請求重定向到 /login?error,後面的Filter就不會走了,所以這個請求過程中起作用的Filter只有一個 UsernamePasswordAuthenticationFilter
,SecurityContextPersistenceFilter
打了個醬油就完了,可以簡單相像成走了下面幾個Filter(實際這個Filter之前的Filter也會進入的,但作用不是鑑權,有可能是防止攻擊一類的,如果cors,或者什麼都沒做)
第三種情況,對了,我想先說第三種情況,就是未登陸訪問任意的受限資源,在進入 AnonymousAuthenticationFilter
之前的Filter都沒有什麼用,在這個Filter中會給這次請求生成一個 匿名訪問用戶的 Authentication
。 然後到 ExceptionTranslationFilter
,暫時不做什麼,等着後面的Filter出錯,如果拋Exception他也沒有什麼用。最後是 FilterSecurityInterceptor
, 他會根據 Authentication
和你的Security配置請求判斷這次請求是權限不足,或是因爲沒有登陸不能訪問,這兩種不能訪問的情況會分別拋出 AccessDeniedException
和 AuthenticationException
,然後Filter鏈回到 ExceptionTranslationFilter
,對這兩種 Exception
進行不同的處理, 對於 AuthenticationException
會被重定向到 /login,對於 AccessDeniedException
會響應一個 403消息
第二種情況,對了最後說第二種情況,我覺得第二種情況是爲了使Security的鑑權流程完整補充上去的,實際不會有人用到他,或者一定會替換他,第一種情況和第二種情況在重定向到login的時候,DefaultLoginPageGeneratingFilter
會生成一個login靜態頁面,好主用戶有一個login的入口
所以就我淺淺的看法來看,真正生產我會只要保證第二種情況就可以了第一和第三種情況一般會通過接口和更美麗的頁面實現,而不會在Filter中實現。
簡單的擴展
基於前面的說明,那我們針對前面一節說的三種情況逐個進行對Security的擴展 。這次我們結合jwt的鑑權方式進行擴展 第一和第三種情況簡單,將登陸頁面和登陸接口兩個 請求路徑放開就可以了,這裏有一個特別說明,對於放開的配置最好用下面的配置,這會使放開的url不進入SecurityFilter
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.mvcMatchers("/login/ps")
.mvcMatchers("/login/ps/refresh")
.mvcMatchers("/login");
}
}
然後自己實現登陸畫面和接口
對於第二種情況需要實現的可能多一點
對應前面說的這個流程針對每一步的Filter進行改造就可以了
SecurityContextPersistenceFilter擴展
前面說了,這個類用於加載SecurityContext,默認他是保存在Session中的,而這個類持有一個SecurityContextRepository對象,這個對象負責具體的保存和加載SecurityContext的邏輯。如果我們使用token 保存身份信息,那麼需要實現一個自己的SecurityContextRepository就可以了。
AnonymousAuthenticationFilter
這個不用動,它挺好的
ExceptionTranslationFilter
需要對這個類持有的兩個對象進行改自定義
- AuthenticationEntryPoint
當沒有登陸時會進入這個類,你需要實現如果沒有登陸的情況下如何處理請求就可以 ,302 or 401
- AccessDeniedHandler
當沒有足夠權限的情況下會進入這個類,你需要實現如果沒有登陸的情況下如何處理請求就可以 ,302 or 403
- FilterSecurityInterceptor
這個類我認爲不太好改造,它需要注入的範圍很多,但我們可以消減他的權力,正常我們會這樣配置Security
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.mvcMatchers("/system/api1").hasAnyRole("ROLE_ADMIN")
.mvcMatchers("/system/api2").hasAnyRole("ROLE_CUSTOM")
.anyRequest().authenticated()
}
}
那麼 FilterSecurityInterceptor
會對每個請求進行權限(hasAnyRole)和登陸兩個認證(authenticated),我們去掉 hasAnyRole 那兩個配置,讓他只進行登陸認證就可以了,另外的功能自定義一個Filter實現
其次,這個類需要一個 AuthenticationProvider
前面寫過這個類是用於具體獲取用戶身份的對象,比如查數據庫,我們需要實現這個類,通過token 來獲得用戶的 Authenticaton
對象(你可能也會自定義一個 Authentication
),然後注入到 FilterSecurityInterceptor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(
new YourAuthenticationProvider()
);
}}
自定義FilterSecurityInterceptor
前面說對於默認的FilterSecurityInterceptor,將它配置成只負責登陸認證,不負責權限認證,那麼我們就需要再添加一個認證權限的Filter就可以了,位置是在 FilterSecurityInterceptor的後面,你只要保證自定義的Filter在訪問者沒有權限的情況下拋出 AccessDeniedException就可以了,具體怎麼鑑權,請發揮想你