天天看點

聊聊 OAuth 2.0 的 Token 續期處理

Token 校驗邏輯

// CheckTokenEndpoint.checkToken
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
    
    // 根據 token 查詢儲存在 tokenStore 的令牌全部資訊
    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
        throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
        throw new InvalidTokenException("Token has expired");
    }
    
    // 根據 token 查詢儲存的 認證資訊 還有權限角色等 (業務資訊)
    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

    return accessTokenConverter.convertAccessToken(token, authentication);
}           
    1. 當用戶端帶着

      header token

      通路

      oauth2

      資源伺服器,資源伺服器會自動攔截

      token

    1. 發送

      token

      到 認證伺服器 校驗

      token

      合法性
    1. 若認證伺服器傳回給資源伺服器是

      token

      不合法,則資源伺服器傳回給用戶端對應的資訊

Token 生成邏輯

//DefaultTokenServices.createAccessToken 代碼邏輯
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
        
        // 根據使用者資訊(username),查詢已下發的token 
        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        
        // 存在已下發的token
        if (existingAccessToken != null) {
            // 1. token 已經被标志過期,則删除 
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // 直接傳回存在的 token,并儲存一下token 和 使用者資訊的關系 (username)
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // 不存在則建立新的 token
        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;

    }
           
    1. 當我們通過oauth2 去擷取

      token

      時,若目前使用者已經存在對應的token,直接傳回而不不會建立新 token。
    1. 這就意味着,雖然設定了對應用戶端擷取 token 的有效時間,這裡擷取到的

      token

      若是已下發舊

      token

      ,有效時間不會和

      session

      機制一樣自動續期。
    1. 綜上情況,在操作過程中

      token

      過期是一個常态化的問題。

Token 重新整理邏輯

curl --location --request POST 'http://auth-server/oauth/token?grant_type=refresh_token' \
--header 'Authorization: Basic dGVzdDp0ZXN0' \
--header 'VERSION: dev' \
--data-urlencode 'scope=server' \
--data-urlencode 'refresh_token=eccda61e-0c68-43af-8f67-6302cb389612'           

若上,當 前端拿着正确的(未過期且未使用過)

refresh_token

去調用 認證中心的重新整理 端點重新整理時,會 觸發

RefreshTokenGranter

, 傳回新的

Token

public class RefreshTokenGranter extends AbstractTokenGranter {
    
    @Override
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        String refreshToken = tokenRequest.getRequestParameters().get("refresh_token");
        return getTokenServices().refreshAccessToken(refreshToken, tokenRequest);
    }
    
}
           
  • refreshAccessToken 代碼實作,調用

    tokenStore

    生成新的

    token

@Transactional(noRollbackFor={InvalidTokenException.class, InvalidGrantException.class})
    public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)
            throws AuthenticationException {

    
       createRefreshedAuthentication(authentication, tokenRequest);

        if (!reuseRefreshToken) {
            tokenStore.removeRefreshToken(refreshToken);
            refreshToken = createRefreshToken(authentication);
        }

        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        if (!reuseRefreshToken) {
            tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);
        }
        return accessToken;
    }           

用戶端(前端)何時重新整理

被動重新整理

聊聊 OAuth 2.0 的 Token 續期處理
  1. 用戶端攜帶

    token

    通路資源伺服器資源
  2. 資源伺服器攔截

    token

    去認證伺服器

    check_token

  3. 認證伺服器傳回

    token

    過期錯誤,資源伺服器包裝錯誤資訊傳回給用戶端
  4. 用戶端根據傳回錯誤資訊(響應碼),直接調用認證伺服器

    refresh_token

  5. 認證伺服器傳回新的

    token

    給用戶端, 然後再次發起 資源調用

被動請求的缺點是,使用者當次請求會失敗(傳回token失敗),對一些業務連貫的操作不是很友好

主動重新整理

聊聊 OAuth 2.0 的 Token 續期處理
  1. 用戶端存在計算邏輯,計算下發token 有效期
  2. 若token要過期之前,主動發起重新整理

主動請求的缺點是,用戶端占用部分計算資源來處理

token

失效問題

// 10S檢測token 有效期
  refreshToken() {
    this.refreshTime = setInterval(() => {
      const token = getStore({
        name: 'access_token',
        debug: true
      })
      if (this.validatenull(token)) {
        return
      }
      if (this.expires_in <= 1000 && !this.refreshLock) {
        this.refreshLock = true
        this.$store
          .dispatch('RefreshToken')
          .catch(() => {
            clearInterval(this.refreshTime)
          })
        this.refreshLock = false
      }
      this.$store.commit('SET_EXPIRES_IN', this.expires_in - 10)
    }, 10000)
  },           
源碼 基于Spring Boot 2.2、 Spring Cloud Hoxton & Alibaba、 OAuth2 的RBAC 權限管理系統