JWT Spring-security

JWT設(shè)計原理 JWT結(jié)合spring-security在項目中的應(yīng)用

JWT譯文

  • 什么是JWT
whatIsJWT.jpg
1. 開放標(biāo)準(zhǔn)
2. 數(shù)字簽名 支持HMAC,RSA,ECDSA加密
3. 驗簽可以保證token的完整性即當(dāng)token內(nèi)容被篡改的時候可以通過驗簽發(fā)現(xiàn)
4. 當(dāng)使用加密后可以保證token內(nèi)容不外泄,僅持有私鑰的一方才能將token解開
  • 什么時候用JWT
whenUseJWT.jpg
1. 鑒權(quán) 支持單點登錄 開銷小 方便跨域
2. 信息交換 JWT支持加密簽名 所以可以安全的傳遞信息 可做驗簽和解密驗證發(fā)送方是否可靠
  • JWT的標(biāo)準(zhǔn)結(jié)構(gòu)應(yīng)該是什么樣的
whatIsStructure.jpg
1. JWT分為三段 頭信息  負載信息  簽名
2. 頭信息 通常由簽名算法+令牌類型組成
3. 中部有效負載
    1. 推薦添加到期時間和主題等信息
    2. 可以任意添加信息 但是注意如果非加密方式的token  建議token內(nèi)不要包含敏感信息  因為token是暴露在外的
4. 簽名 需要將頭信息和負載內(nèi)容一起做簽名 驗簽的時候可以避免信息被篡改

SPRING-SECURITY譯文

  • spring-security

  • 特性


    features.jpg
    1. 支持身份驗證,授權(quán),防范常見攻擊
    2. 支持集成
  • 基礎(chǔ)組件


    component.jpg
    • SecurityContextHolder 存儲和獲取驗證后信息
      SecurityContextHolder.getContext().getAuthentication();
    
    • SecurityContext 從SecurityContextHolder中獲得的上下文信息 包含認證信息
    • Authentication 不同階段的鑒權(quán)對象 如:鑒權(quán)后的當(dāng)前登陸人或鑒權(quán)前的PreAuthenticatedAuthenticationToken(預(yù)處理攔截器先處理得到預(yù)處理token再調(diào)用AuthenticationManager得到最終token)
    • GrantedAuthority 授予鑒權(quán)對象的權(quán)限 如:角色 范圍等
    • AuthenticationManager 具體Filter如何執(zhí)行身份驗證的API
    • ProviderManager 是AuthenticationManager的具體實現(xiàn)
      • 首先實現(xiàn)AuthenticationProvider,注意里面的support方法 決定了Provider到底處理那種類型的Authentication,如上第二點所說Authentication 存在多種類型
    public interface AuthenticationProvider {
    // ~ Methods
    // ========================================================================================================
    
    /**
     * Performs authentication with the same contract as
     * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)}
     * .
     *
     * @param authentication the authentication request object.
     *
     * @return a fully authenticated object including credentials. May return
     * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
     * authentication of the passed <code>Authentication</code> object. In such a case,
     * the next <code>AuthenticationProvider</code> that supports the presented
     * <code>Authentication</code> class will be tried.
     *
     * @throws AuthenticationException if authentication fails.
     */
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    
    /**
     * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the
     * indicated <Code>Authentication</code> object.
     * <p>
     * Returning <code>true</code> does not guarantee an
     * <code>AuthenticationProvider</code> will be able to authenticate the presented
     * instance of the <code>Authentication</code> class. It simply indicates it can
     * support closer evaluation of it. An <code>AuthenticationProvider</code> can still
     * return <code>null</code> from the {@link #authenticate(Authentication)} method to
     * indicate another <code>AuthenticationProvider</code> should be tried.
     * </p>
     * <p>
     * Selection of an <code>AuthenticationProvider</code> capable of performing
     * authentication is conducted at runtime the <code>ProviderManager</code>.
     * </p>
     *
     * @param authentication
     *
     * @return <code>true</code> if the implementation can more closely evaluate the
     * <code>Authentication</code> class presented
     */
    boolean supports(Class<?> authentication);
    }
    
     - 其次ProviderManager的authenticate會遍歷所有Provider(getProviders),然后找到上面提到的支持當(dāng)前Authentication類型的Provider做處理
    
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();
    
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
    
            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }
    
    • AuthenticationProvider ProviderManager眾多執(zhí)行者中的一個,如上面所講,滿足類型的AuthenticationProvider將被執(zhí)行

    • AuthenticationEntryPoint 對于鑒權(quán)過程中如異常等響應(yīng)的統(tǒng)一處理

    • AbstractAuthenticationProcessingFilter


      abstractAuthFilter.jpg
      • 以UsernamePasswordAuthenticationFilter為例,主要是實現(xiàn)attemptAuthentication方法將request中的參數(shù)進行封裝,變?yōu)锳uthentication,再傳遞給下游AuthenticationManager
    • DaoAuthenticationProvider

      daoAuth.jpg
      • DaoAuthenticationProvider會從UserDetailsService中加載用戶信息,然后與傳遞過來的用戶名密碼進行比較
      //如何定義DaoAuthenticationProvider及注入UserDetailsService
      //繼承WebSecurityConfigurerAdapter并重寫configure方法
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //第一次登陸賬號密碼驗證Provider
            //默認使用BCryptPasswordEncoder比對加密后的密碼  daoProvider.setPasswordEncoder();
            //驗證方法為spring-security內(nèi)部提供的DaoAuthenticationProvider.additionalAuthenticationChecks
            DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
            daoProvider.setUserDetailsService(jwtUserDetailsService);
            daoProvider.setPasswordEncoder(new Md5PasswordEncoder());
            //定義兩個Provider  daoProvider負責(zé)UserNameAndPasswordToken登錄驗證
            auth.authenticationProvider(daoProvider);
        }
      
    • UserDetailsService 獲取當(dāng)前登錄用戶信息,實現(xiàn)UserDetailsService然后返回UserDetails

    public interface UserDetailsService {
    // ~ Methods
    // ========================================================================================================
    
    /**
     * Locates the user based on the username. In the actual implementation, the search
     * may possibly be case sensitive, or case insensitive depending on how the
     * implementation instance is configured. In this case, the <code>UserDetails</code>
     * object that comes back may have a username that is of a different case than what
     * was actually requested..
     *
     * @param username the username identifying the user whose data is required.
     *
     * @return a fully populated user record (never <code>null</code>)
     *
     * @throws UsernameNotFoundException if the user could not be found or the user has no
     * GrantedAuthority
     */
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }
    
    
    • FilterInvocationSecurityMetadataSource 為當(dāng)前請求的URL打上一些標(biāo)簽,如:當(dāng)前的URL需要什么資源可以訪問,ConfigAttribute為接口可以自己定義實現(xiàn)
      @Override
      public Collection<ConfigAttribute> getAttributes(Object o) {  
          //FilterInvocation filterInvocation=Object o; 獲取當(dāng)前request
          //當(dāng)前URL的特殊標(biāo)簽  
          //獲取什么資源可以允許當(dāng)前request然后將資源id封裝后返回
      }
    
      @Override
      public Collection<ConfigAttribute> getAllConfigAttributes() {
         //全局標(biāo)簽
          return Collections.emptyList();
      }
    
      @Override
      public boolean supports(Class<?> aClass) {
          //什么類型的請求可以走此封裝
          return FilterInvocation.class.isAssignableFrom(aClass);
      }
    
    • AccessDecisionManager 授權(quán)決策接口 跟FilterInvocationSecurityMetadataSource配套使用
    //根據(jù)之前提到的AuthenticationManager封裝的Authentication中的角色信息及FilterInvocationSecurityMetadataSource中的請求標(biāo)簽 判斷當(dāng)前的角色是否有操作resourceIds的權(quán)限
    public void decide(Authentication auth, Object o, Collection<ConfigAttribute> resourceIds)
    
    //開啟自定義資源認證
    //@EnableWebSecurity
    //public class WebSecurityConfig extends WebSecurityConfigurerAdapter
      @Override
      protected void configure(HttpSecurity http) throws Exception {
          //customMetadataSourceService每次請求根據(jù)數(shù)據(jù)庫配置讀取資源元信息及所需權(quán)限 并通過urlAccessDecisionManager與當(dāng)前登錄人所包含的權(quán)限進行比對
          http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
              @Override
              public <O extends FilterSecurityInterceptor> O postProcess(O o) {
                  o.setSecurityMetadataSource(customMetadataSourceService);
                  o.setAccessDecisionManager(urlAccessDecisionManager);
                  return o;
              }
          }).anyRequest().permitAll()
    
    • AuthenticationSuccessHandler 請求成功后處理 這個就不詳細介紹了
  • 認證機制


    authenticationMechanisms.jpg
    • 因為我們主要說JWT所以簡單說一下 Pre-Authentication Scenarios 當(dāng)已經(jīng)做了外部鑒權(quán),到spring-security直接可用,即預(yù)驗證場景
      • 首先需要實現(xiàn)AbstractPreAuthenticatedProcessingFilter,這里主要是實現(xiàn)方法getPreAuthenticatedPrincipal,從request中獲取預(yù)授權(quán)信息
      • setCheckForPrincipalChanges(true),用來保證security上下文發(fā)生變更時候會走此預(yù)授權(quán)
      • AbstractPreAuthenticatedProcessingFilter內(nèi)部會將principal封裝成PreAuthenticatedAuthenticationToken(Authentication)并傳遞給下游AuthenticationManager
      • AuthenticationManager完成驗證并返回實際Authentication將會存在SecurityContextHolder中便于在系統(tǒng)中獲取當(dāng)前人員
  • 上圖 圖1是普通登錄生成token的過程 圖2為使用token進行鑒權(quán)的過程


    common-login.jpg

    common-jwt.jpg
  • 對于JWT實現(xiàn)方式的一些探討 能否借助redis做密鑰生成 滿足自動過期和僅允許一人登錄 答案是可以的 下面就分幾步簡單介紹一下

    • header和payload不做探討了 就是標(biāo)準(zhǔn)結(jié)構(gòu) 兩個JSON 且不包含敏感信息
    • 首先根據(jù)用戶名+UUID(或任意比較復(fù)雜的隨機方案) 生成一個當(dāng)前用戶的secret 并將secret保存在redis 如JWT_AAA_SEC=***
    • 然后將header和payload+secret通過hmacSha256Base64做一個簽名為sign token為base64 header . base64 payload . sign
    • 當(dāng)有請求時 首先根據(jù)username從redis中獲取secret
    • 然后重復(fù)3中步驟生成sign并與當(dāng)前token的sign做比較 如果不一致驗簽失敗
    • 那重復(fù)登錄踢出和自動過期的實現(xiàn)方式就很顯然了 不詳細說了
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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