大家好,我是不才陳某~
認證、授權是實戰專案中必不可少的部分,而Spring Security則將作為首選安全元件,因此陳某新開了 《Spring Security 進階》 這個專欄,寫一寫從單體架構到OAuth2分散式架構的認證授權。
Spring security這裡就不再過多介紹了,相信大家都用過,也都恐懼過,相比Shiro而言,Spring Security更加重量級,之前的SSM專案更多企業都是用的Shiro,但是Spring Boot出來之後,整合Spring Security更加方便了,用的企業也就多了。
今天陳某就來介紹一下在前後端分離的專案中如何使用Spring Security進行登入認證。文章的目錄如下:
前後端分離認證的思路
前後端分離不同於傳統的web服務,無法使用session,因此我們採用JWT這種無狀態機制來生成token,大致的思路如下:
客戶端呼叫服務端登入介面,輸入使用者名稱、密碼登入,登入成功返回兩個token,如下:
- accessToken:客戶端攜帶這個token訪問服務端的資源
- refreshToken:重新整理令牌,一旦accessToken過期了,客戶端需要使用refreshToken重新獲取一個accessToken。因此refreshToken的過期時間一般大於accessToken。
- 客戶請求頭中攜帶accessToken訪問服務端的資源,服務端對accessToken進行鑑定(驗籤、是否失效....),如果這個accessToken沒有問題則放行。
- accessToken一旦過期需要客戶端攜帶refreshToken呼叫重新整理令牌的介面重新獲取一個新的accessToken。
專案搭建
陳某使用的是Spring Boot 框架,演示專案新建了兩個模組,分別是common-base
、security-authentication-jwt
。
1、common-base模組
這是一個抽象出來的公共模組,這個模組主要放一些公用的類,目錄如下:
2、security-authentication-jwt模組
一些需要定製的類,比如security的全域性配置類、Jwt登入過濾器的配置類,目錄如下:
3、五張表
許可權設計根據業務的需求往往有不同的設計,陳某用的RBAC規範,主要涉及到五張表,分別是使用者表、角色表、許可權表、使用者<->角色表、角色<->許可權表,如下圖:
上述幾張表的SQL會放在案例原始碼中(這幾張表欄位為了省事,設計的並不全,自己根據業務逐步擴充即可)
登入認證過濾器
登入介面的邏輯寫法有很多種,今天陳某介紹一種使用過濾器的定義的登入介面。
Spring Security預設的表單登入認證的過濾器是UsernamePasswordAuthenticationFilter
,這個過濾器並不適用於前後端分離的架構,因此我們需要自定義一個過濾器。
邏輯很簡單,參照UsernamePasswordAuthenticationFilter
這個過濾器改造一下,程式碼如下:
認證成功處理器AuthenticationSuccessHandler
上述的過濾器介面一旦認證成功,則會呼叫AuthenticationSuccessHandler進行處理,因此我們可以自定義一個認證成功處理器進行自己的業務處理,程式碼如下:
陳某僅僅返回了accessToken、refreshToken,其他的業務邏輯處理自己完善。
認證失敗處理器AuthenticationFailureHandler
同樣的,一旦登入失敗,比如使用者名稱或者密碼錯誤等等,則會呼叫AuthenticationFailureHandler進行處理,因此我們需要自定義一個認證失敗的處理器,其中根據異常資訊返回特定的JSON資料給客戶端,程式碼如下:
邏輯很簡單,AuthenticationException有不同的實現類,根據異常的型別返回特定的提示資訊即可。
AuthenticationEntryPoint配置
AuthenticationEntryPoint這個介面當使用者未通過認證訪問受保護的資源時,將會呼叫其中的commence()
方法進行處理,比如客戶端攜帶的token被篡改,因此我們需要自定義一個AuthenticationEntryPoint返回特定的提示資訊,程式碼如下:
AccessDeniedHandler配置
AccessDeniedHandler這處理器當認證成功的使用者訪問受保護的資源,但是許可權不夠,則會進入這個處理器進行處理,我們可以實現這個處理器返回特定的提示資訊給客戶端,程式碼如下:
UserDetailsService配置
UserDetailsService這個類是用來載入使用者資訊,包括使用者名稱、密碼、許可權、角色集合....其中有一個方法如下:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
在認證邏輯中Spring Security會呼叫這個方法根據客戶端傳入的username載入該使用者的詳細資訊,這個方法需要完成的邏輯如下:
- 密碼匹配
- 載入許可權、角色集合
我們需要實現這個介面,從資料庫載入使用者資訊,程式碼如下:
其中的LoginService是根據使用者名稱從資料庫中查詢出密碼、角色、許可權,程式碼如下:
UserDetails這個也是個介面,其中定義了幾種方法,都是圍繞著使用者名稱、密碼、許可權+角色集合這三個屬性,因此我們可以實現這個類擴充這些欄位,SecurityUser程式碼如下:
擴充:UserDetailsService這個類的實現一般涉及到5張表,分別是使用者表、角色表、許可權表、使用者<->角色對應關係表、角色<->許可權對應關係表,企業中的實現必須遵循RBAC設計規則。這個規則陳某後面會詳細介紹。
Token校驗過濾器
客戶端請求頭攜帶了token,服務端肯定是需要針對每次請求解析、校驗token,因此必須定義一個Token過濾器,這個過濾器的主要邏輯如下:
- 從請求頭中獲取accessToken
- 對accessToken解析、驗籤、校驗過期時間
- 校驗成功,將authentication存入ThreadLocal中,這樣方便後續直接獲取使用者詳細資訊。
上面只是最基礎的一些邏輯,實際開發中還有特定的處理,比如將使用者的詳細資訊放入Request屬性中、Redis快取中,這樣能夠實現feign的令牌中繼效果。
校驗過濾器的程式碼如下:
重新整理令牌介面
accessToken一旦過期,客戶端必須攜帶著refreshToken重新獲取令牌,傳統web服務是放在cookie中,只需要服務端完成重新整理,完全做到無感知令牌續期,但是前後端分離架構中必須由客戶端拿著refreshToken調介面手動重新整理。
程式碼如下:
主要邏輯很簡單,如下:
- 校驗refreshToken
- 重新生成accessToken、refreshToken返回給客戶端。
注意:實際生產中refreshToken令牌的生成方式、加密演算法可以和accessToken不同。
登入認證過濾器介面配置
上述定義了一個認證過濾器JwtAuthenticationLoginFilter,這個是用來登入的過濾器,但是並沒有注入加入Spring Security的過濾器鏈中,需要定義配置,程式碼如下:
/**
* @author 公號:碼猿技術專欄
* 登入過濾器的配置類
*/
@Configuration
public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
/**
* userDetailService
*/
@Qualifier("jwtTokenUserDetailsService")
@Autowired
private UserDetailsService userDetailsService;
/**
* 登入成功處理器
*/
@Autowired
private LoginAuthenticationSuccessHandler loginAuthenticationSuccessHandler;
/**
* 登入失敗處理器
*/
@Autowired
private LoginAuthenticationFailureHandler loginAuthenticationFailureHandler;
/**
* 加密
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 將登入介面的過濾器配置到過濾器鏈中
* 1. 配置登入成功、失敗處理器
* 2. 配置自定義的userDetailService(從資料庫中獲取使用者資料)
* 3. 將自定義的過濾器配置到spring security的過濾器鏈中,配置在UsernamePasswordAuthenticationFilter之前
* @param http
*/
@Override
public void configure(HttpSecurity http) {
JwtAuthenticationLoginFilter filter = new JwtAuthenticationLoginFilter();
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//認證成功處理器
filter.setAuthenticationSuccessHandler(loginAuthenticationSuccessHandler);
//認證失敗處理器
filter.setAuthenticationFailureHandler(loginAuthenticationFailureHandler);
//直接使用DaoAuthenticationProvider
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
//設定userDetailService
provider.setUserDetailsService(userDetailsService);
//設定加密演算法
provider.setPasswordEncoder(passwordEncoder);
http.authenticationProvider(provider);
//將這個過濾器新增到UsernamePasswordAuthenticationFilter之前執行
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
}
所有的邏輯都在public void configure(HttpSecurity http)
這個方法中,如下:
- 設定認證成功處理器loginAuthenticationSuccessHandler
- 設定認證失敗處理器loginAuthenticationFailureHandler
- 設定userDetailService的實現類JwtTokenUserDetailsService
- 設定加密演算法passwordEncoder
- 將JwtAuthenticationLoginFilter這個過濾器加入到過濾器鏈中,直接加入到UsernamePasswordAuthenticationFilter這個過濾器之前。
Spring Security全域性配置
上述僅僅配置了登入過濾器,還需要在全域性配置類做一些配置,如下:
- 應用登入過濾器的配置
- 將登入介面、令牌重新整理介面放行,不需要攔截
- 配置AuthenticationEntryPoint、AccessDeniedHandler
- 禁用session,前後端分離+JWT方式不需要session
- 將token校驗過濾器TokenAuthenticationFilter新增到過濾器鏈中,放在UsernamePasswordAuthenticationFilter之前。
完整配置如下:
/**
* @author 公眾號:碼猿技術專欄
* @EnableGlobalMethodSecurity 開啟許可權校驗的註解
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig;
@Autowired
private EntryPointUnauthorizedHandler entryPointUnauthorizedHandler;
@Autowired
private RequestAccessDeniedHandler requestAccessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
//禁用表單登入,前後端分離用不上
.disable()
//應用登入過濾器的配置,配置分離
.apply(jwtAuthenticationSecurityConfig)
.and()
// 設定URL的授權
.authorizeRequests()
// 這裡需要將登入頁面放行,permitAll()表示不再攔截,/login 登入的url,/refreshToken重新整理token的url
//TODO 此處正常專案中放行的url還有很多,比如swagger相關的url,druid的後臺url,一些靜態資源
.antMatchers( "/login","/refreshToken")
.permitAll()
//hasRole()表示需要指定的角色才能訪問資源
.antMatchers("/hello").hasRole("ADMIN")
// anyRequest() 所有請求 authenticated() 必須被認證
.anyRequest()
.authenticated()
//處理異常情況:認證失敗和許可權不足
.and()
.exceptionHandling()
//認證未通過,不允許訪問異常處理器
.authenticationEntryPoint(entryPointUnauthorizedHandler)
//認證通過,但是沒許可權處理器
.accessDeniedHandler(requestAccessDeniedHandler)
.and()
//禁用session,JWT校驗不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//將TOKEN校驗過濾器配置到過濾器鏈中,否則不生效,放到UsernamePasswordAuthenticationFilter之前
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class)
// 關閉csrf
.csrf().disable();
}
// 自定義的Jwt Token校驗過濾器
@Bean
public TokenAuthenticationFilter authenticationTokenFilterBean() {
return new TokenAuthenticationFilter();
}
/**
* 加密演算法
* @return
*/
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
註釋的很詳細了,有不理解的認真看一下。
案例原始碼已經上傳GitHub,關注公眾號:碼猿技術專欄,回覆關鍵詞:9529 獲取!
測試
1、首先測試登入介面,postman訪問http://localhost:2001/securit...,如下:
可以看到,成功返回了兩個token。
2、請求頭不攜帶token,直接請求http://localhost:2001/securit...,如下:
可以看到,直接進入了EntryPointUnauthorizedHandler這個處理器。
3、攜帶token訪問http://localhost:2001/securit...,如下:
成功訪問,token是有效的。
4、重新整理令牌介面測試,攜帶一個過期的令牌訪問如下:
5、重新整理令牌介面測試,攜帶未過期的令牌測試,如下:
可以看到,成功返回了兩個新的令牌。
原始碼追蹤
以上一系列的配置完全是參照UsernamePasswordAuthenticationFilter這個過濾器,這個是web服務表單登入的方式。
Spring Security的原理就是一系列的過濾器組成,登入流程也是一樣,起初在org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter()
方法,進行認證匹配,如下:
attemptAuthentication()
這個方法主要作用就是獲取客戶端傳遞的username、password,封裝成UsernamePasswordAuthenticationToken
交給ProviderManager
的進行認證,原始碼如下:
ProviderManager主要流程是呼叫抽象類AbstractUserDetailsAuthenticationProvider#authenticate()
方法,如下圖:
retrieveUser()
方法就是呼叫userDetailService查詢使用者資訊。然後認證,一旦認證成功或者失敗,則會呼叫對應的失敗、成功處理器進行處理。
總結
Spring Security雖然比較重,但是真的好用,尤其是實現Oauth2.0規範,非常簡單方便。
案例原始碼已經上傳GitHub,關注公眾號:碼猿技術專欄,回覆關鍵詞:9529 獲取!
最後說一句(別白嫖,求關注)
陳某每一篇文章都是精心輸出,已經寫了3個專欄,整理成PDF,獲取方式如下:
- 《Spring Cloud 進階》PDF:關注公號:【碼猿技術專欄】回覆關鍵詞 Spring Cloud 進階 獲取!
- 《Spring Boot 進階》PDF:關注公號:【碼猿技術專欄】回覆關鍵詞 Spring Boot進階 獲取!
- 《Mybatis 進階》PDF:關注公號:【碼猿技術專欄】回覆關鍵詞 Mybatis 進階 獲取!
如果這篇文章對你有所幫助,或者有所啟發的話,幫忙點贊、在看、轉發、收藏,你的支援就是我堅持下去的最大動力!
關注公號:【碼猿技術專欄】,公眾號內有超讚的粉絲福利,回覆:加群,可以加入技術討論群,和大家一起討論技術,吹牛逼!