大家好,我是不才陳某~
認證、授權是實戰項目中必不可少的部分,而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/security-jwt/login,如下:
可以看到,成功返回了兩個token。
2、請求頭不攜帶token,直接請求http://localhost:2001/security-jwt/hello,如下:
可以看到,直接進入了EntryPointUnauthorizedHandler這個處理器。
3、攜帶token訪問http://localhost:2001/security-jwt/hello,如下:
成功訪問,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 進階 獲取!
如果這篇文章對你有所幫助,或者有所啟發的話,幫忙點贊、在看、轉發、收藏,你的支持就是我堅持下去的最大動力!