JWT Spring-security

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

JWT譯文

  • 什么是JWT
whatIsJWT.jpg
1. 開放標準
2. 數(shù)字簽名 支持HMAC屯蹦,RSA鳄梅,ECDSA加密
3. 驗簽可以保證token的完整性即當token內(nèi)容被篡改的時候可以通過驗簽發(fā)現(xiàn)
4. 當使用加密后可以保證token內(nèi)容不外泄状勤,僅持有私鑰的一方才能將token解開
  • 什么時候用JWT
whenUseJWT.jpg
1. 鑒權(quán) 支持單點登錄 開銷小 方便跨域
2. 信息交換 JWT支持加密簽名 所以可以安全的傳遞信息 可做驗簽和解密驗證發(fā)送方是否可靠
  • JWT的標準結(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)后的當前登陸人或鑒權(quán)前的PreAuthenticatedAuthenticationToken(預處理攔截器先處理得到預處理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),然后找到上面提到的支持當前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負責UserNameAndPasswordToken登錄驗證
            auth.authenticationProvider(daoProvider);
        }
      
    • UserDetailsService 獲取當前登錄用戶信息,實現(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 為當前請求的URL打上一些標簽,如:當前的URL需要什么資源可以訪問,ConfigAttribute為接口可以自己定義實現(xiàn)
      @Override
      public Collection<ConfigAttribute> getAttributes(Object o) {  
          //FilterInvocation filterInvocation=Object o; 獲取當前request
          //當前URL的特殊標簽  
          //獲取什么資源可以允許當前request然后將資源id封裝后返回
      }
    
      @Override
      public Collection<ConfigAttribute> getAllConfigAttributes() {
         //全局標簽
          return Collections.emptyList();
      }
    
      @Override
      public boolean supports(Class<?> aClass) {
          //什么類型的請求可以走此封裝
          return FilterInvocation.class.isAssignableFrom(aClass);
      }
    
    • AccessDecisionManager 授權(quán)決策接口 跟FilterInvocationSecurityMetadataSource配套使用
    //根據(jù)之前提到的AuthenticationManager封裝的Authentication中的角色信息及FilterInvocationSecurityMetadataSource中的請求標簽 判斷當前的角色是否有操作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與當前登錄人所包含的權(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 當已經(jīng)做了外部鑒權(quán)示罗,到spring-security直接可用,即預驗證場景
      • 首先需要實現(xiàn)AbstractPreAuthenticatedProcessingFilter芝硬,這里主要是實現(xiàn)方法getPreAuthenticatedPrincipal蚜点,從request中獲取預授權(quán)信息
      • setCheckForPrincipalChanges(true),用來保證security上下文發(fā)生變更時候會走此預授權(quán)
      • AbstractPreAuthenticatedProcessingFilter內(nèi)部會將principal封裝成PreAuthenticatedAuthenticationToken(Authentication)并傳遞給下游AuthenticationManager
      • AuthenticationManager完成驗證并返回實際Authentication將會存在SecurityContextHolder中便于在系統(tǒng)中獲取當前人員
  • 上圖 圖1是普通登錄生成token的過程 圖2為使用token進行鑒權(quán)的過程


    common-login.jpg

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

    • header和payload不做探討了 就是標準結(jié)構(gòu) 兩個JSON 且不包含敏感信息
    • 首先根據(jù)用戶名+UUID(或任意比較復雜的隨機方案) 生成一個當前用戶的secret 并將secret保存在redis 如JWT_AAA_SEC=***
    • 然后將header和payload+secret通過hmacSha256Base64做一個簽名為sign token為base64 header . base64 payload . sign
    • 當有請求時 首先根據(jù)username從redis中獲取secret
    • 然后重復3中步驟生成sign并與當前token的sign做比較 如果不一致驗簽失敗
    • 那重復登錄踢出和自動過期的實現(xiàn)方式就很顯然了 不詳細說了
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吵取,一起剝皮案震驚了整個濱河市禽额,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖脯倒,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件实辑,死亡現(xiàn)場離奇詭異,居然都是意外死亡藻丢,警方通過查閱死者的電腦和手機剪撬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悠反,“玉大人残黑,你說我怎么就攤上這事≌瘢” “怎么了梨水?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茵臭。 經(jīng)常有香客問我疫诽,道長,這世上最難降的妖魔是什么旦委? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任奇徒,我火速辦了婚禮,結(jié)果婚禮上缨硝,老公的妹妹穿的比我還像新娘摩钙。我一直安慰自己,他們只是感情好查辩,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布胖笛。 她就那樣靜靜地躺著,像睡著了一般宜肉。 火紅的嫁衣襯著肌膚如雪匀钧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天谬返,我揣著相機與錄音之斯,去河邊找鬼。 笑死遣铝,一個胖子當著我的面吹牛佑刷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酿炸,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼瘫絮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了填硕?” 一聲冷哼從身側(cè)響起麦萤,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鹿鳖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后壮莹,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翅帜,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年命满,在試婚紗的時候發(fā)現(xiàn)自己被綠了涝滴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡胶台,死狀恐怖歼疮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情诈唬,我是刑警寧澤韩脏,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站铸磅,受9級特大地震影響骤素,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愚屁,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望痕檬。 院中可真熱鬧霎槐,春花似錦、人聲如沸梦谜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽唁桩。三九已至闭树,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荒澡,已是汗流浹背报辱。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留单山,地道東北人碍现。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像米奸,于是被迫代替她去往敵國和親昼接。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容