實戰!spring Boot security+JWT 前后端分離架構認證登錄!


大家好,我是不才陳某~

認證、授權是實戰項目中必不可少的部分,而Spring Security則將作為首選安全組件,因此陳某新開了 《Spring Security 進階》 這個專欄,寫一寫從單體架構到OAuth2分布式架構的認證授權。

Spring security這里就不再過多介紹了,相信大家都用過,也都恐懼過,相比Shiro而言,Spring Security更加重量級,之前的SSM項目更多企業都是用的Shiro,但是Spring Boot出來之后,整合Spring Security更加方便了,用的企業也就多了。

今天陳某就來介紹一下在前后端分離的項目中如何使用Spring Security進行登錄認證。文章的目錄如下:

前后端分離認證的思路

前后端分離不同於傳統的web服務,無法使用session,因此我們采用JWT這種無狀態機制來生成token,大致的思路如下:

  1. 客戶端調用服務端登錄接口,輸入用戶名、密碼登錄,登錄成功返回兩個token,如下:
    1. accessToken:客戶端攜帶這個token訪問服務端的資源
    2. refreshToken:刷新令牌,一旦accessToken過期了,客戶端需要使用refreshToken重新獲取一個accessToken。因此refreshToken的過期時間一般大於accessToken。
  2. 客戶請求頭中攜帶accessToken訪問服務端的資源,服務端對accessToken進行鑒定(驗簽、是否失效....),如果這個accessToken沒有問題則放行。
  3. accessToken一旦過期需要客戶端攜帶refreshToken調用刷新令牌的接口重新獲取一個新的accessToken

項目搭建

陳某使用的是Spring Boot 框架,演示項目新建了兩個模塊,分別是common-basesecurity-authentication-jwt

1、common-base模塊

這是一個抽象出來的公共模塊,這個模塊主要放一些公用的類,目錄如下:

2、security-authentication-jwt模塊

一些需要定制的類,比如security的全局配置類、Jwt登錄過濾器的配置類,目錄如下:

3、五張表

權限設計根據業務的需求往往有不同的設計,陳某用的RBAC規范,主要涉及到五張表,分別是用戶表角色表權限表用戶<->角色表角色<->權限表,如下圖:

上述幾張表的SQL會放在案例源碼中(這幾張表字段為了省事,設計的並不全,自己根據業務逐步拓展即可)

登錄認證過濾器

登錄接口的邏輯寫法有很多種,今天陳某介紹一種使用過濾器的定義的登錄接口。

Spring Security默認的表單登錄認證的過濾器是UsernamePasswordAuthenticationFilter,這個過濾器並不適用於前后端分離的架構,因此我們需要自定義一個過濾器。

邏輯很簡單,參照UsernamePasswordAuthenticationFilter這個過濾器改造一下,代碼如下:

認證成功處理器AuthenticationSuccessHandler

上述的過濾器接口一旦認證成功,則會調用AuthenticationSuccessHandler進行處理,因此我們可以自定義一個認證成功處理器進行自己的業務處理,代碼如下:

陳某僅僅返回了accessTokenrefreshToken,其他的業務邏輯處理自己完善。

認證失敗處理器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
  • 重新生成accessTokenrefreshToken返回給客戶端。

注意:實際生產中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全局配置

上述僅僅配置了登錄過濾器,還需要在全局配置類做一些配置,如下:

  • 應用登錄過濾器的配置
  • 將登錄接口、令牌刷新接口放行,不需要攔截
  • 配置AuthenticationEntryPointAccessDeniedHandler
  • 禁用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,獲取方式如下:

  1. 《Spring Cloud 進階》PDF:關注公號:【碼猿技術專欄】回復關鍵詞 Spring Cloud 進階 獲取!
  2. 《Spring Boot 進階》PDF:關注公號:【碼猿技術專欄】回復關鍵詞 Spring Boot進階 獲取!
  3. 《Mybatis 進階》PDF:關注公號:【碼猿技術專欄】回復關鍵詞 Mybatis 進階 獲取!

如果這篇文章對你有所幫助,或者有所啟發的話,幫忙點贊在看轉發收藏,你的支持就是我堅持下去的最大動力!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM