JWT Token刷新方案


JWT TOKEN刷新方案
一、環境
  Springboot,Redis

二、需求
  最近在做用戶中心,需要向其他服務簽發JWT Token,使用Token來獲取用戶信息,保證用戶信息安全可靠,不會被重放攻擊。

三、問題
  JWT Token設置有效期,一旦失效用戶就要重新登錄,這樣的體驗非常差,需要做到用戶在無感知的情況下,解決如何刷新Token的問題。

四、解決方案
1.設計思路
  看了很多文章,大都是通過refresh Token刷新,其實做法上是類似的。

2.Token設計
說明
  正常Token:Token未過期,且未達到建議更換時間。
  瀕死Token:Token未過期,已達到建議更換時間。
  正常過期Token:Token已過期,但存在於緩存中。
  非正常過期Token:Token已過期,不存在於緩存中。

過期時間
  Token過期時間越短越安全,如設置Token過期時間15分鍾,建議更換時間設置為Token前5分鍾,則Token生命周期如下:

時間 Token類型 說明
0-10分鍾 正常Token 正常訪問
10-15分鍾 瀕死Token 正常訪問,返回新Token,建議使用新Token
>15分鍾 過期Token 需校驗是否正常過期。正常過期則能訪問,並返回新Token;
非過期Token拒絕訪問
生成一個正常Token
在緩存中,通過用戶標識查詢老Token。
如存在,將老Token(Token,用戶標識)本條緩存設置過期時間,作為新老Token交替的過渡期。
將新Token以(Token,用戶標識)、(用戶標識,Token)一對的形式存入緩存,不設置過期時間。
獲取一個正常Token
  在緩存中,通過用戶標識查詢用戶當前Token,校驗該Token是否為正常Token,如正常則返回;不正常則生成一個正常Token。

3.情景
正常Token傳入
  當正常Token請求時,返回當前Token。

瀕死Token傳入
  當瀕死Token請求時,獲取一個正常Token並返回。

正常過期Token
  當正常過期Token請求時,獲取一個正常Token並返回。

非正常過期過期Token
  當非正常過期Token請求時,返回錯誤信息,需重新登錄。

4.代碼
Maven依賴

    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.7.0</version>
    </dependency>

jwt token工具類

/**
     * 獲取用戶從token中
     */
    public String getUserFromToken(String token) {
        return getClaimFromToken(token).getSubject();
    }

    /**
     * <pre>
     *  驗證token是否失效
     *  true:過期   false:沒過期
     * </pre>
     */
    public Boolean isTokenExpired(String token) {
        try {
            final Date expiration = getExpirationDateFromToken(token);
            return expiration.before(new Date());
        } catch (ExpiredJwtException expiredJwtException) {
            return true;
        }
    }

    /**
     * 獲取可用的token
     * 如該用戶當前token可用,即返回
     * 當前token不可用,則返回一個新token
     * @param userId
     * @return
     */
    public String getGoodToken(String userId){
        String token = redisTemplate.opsForValue().get("userJwtToken_"+userId);
        boolean flag = this.checkToken(token);
        //校驗當前token能否使用,不能使用則生成新token
        if(flag){
            return token;
        }else{
            String newToken = this.createToken(userId);
            //初始化新token
            this.initNewToken(userId, newToken);
            return newToken;
        }
    }

    /**
     * 判斷過期token是否合法
     * @param token
     * @return
     */
    public String checkExpireToken(String token){
        //判斷token是否需要更新
        boolean expireFlag = this.checkToken(token);
        //false:不建議使用
        if(!expireFlag){
            String userId = redisTemplate.opsForValue().get(token);
            if(ToolUtil.isNotEmpty(userId)){
                return userId + "-1";
            }
        }else{
            String userId = this.getUserFromToken(token);
            return userId;
        }
        return "";
    }

    /**
     * 檢查當前token是否還能繼續使用
     * true:可以  false:不建議
     * @param token
     * @return
     */
    public boolean checkToken(String token){
        SecretKey secretKey = this.createSecretKey();
        try {
            // jwt正常情況 則判斷失效時間是否大於5分鍾
            long expireTime = Jwts.parser()   //得到DefaultJwtParser
                    .setSigningKey(secretKey)  //設置簽名的秘鑰
                    .parseClaimsJws(token.replace("jwt_", ""))
                    .getBody().getExpiration().getTime();
            long diff = expireTime - System.currentTimeMillis();
            //如果有效期小於5分鍾,則不建議繼續使用該token
            if (diff < ADVANCE_EXPIRE_TIME) {
                return false;
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
     * 創建新token
     * @param userId 用戶ID
     * @return
     */
    public String createToken(String userId){
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        long nowMillis = System.currentTimeMillis();//生成JWT的時間
        Date now = new Date(nowMillis);
//        Map<String,Object> claims = new HashMap<String,Object>();//創建payload的私有聲明(根據特定的業務需要添加,如果要拿這個做驗證,一般是需要和jwt的接收方提前溝通好驗證方式的)
        SecretKey secretKey = createSecretKey();//生成簽名的時候使用的秘鑰secret,這個方法本地封裝了的,一般可以從本地配置文件中讀取,切記這個秘鑰不能外露哦。它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。
        //下面就是在為payload添加各種標准聲明和私有聲明了
        JwtBuilder builder = Jwts.builder() //這里其實就是new一個JwtBuilder,設置jwt的body
//                .setClaims(claims)          //如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標准的聲明賦值之后,就是覆蓋了那些標准的聲明的
                .setId(UUID.randomUUID().toString())                  //設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置為一個不重復的值,主要用來作為一次性token,從而回避重放攻擊。
                .setIssuedAt(now)           //iat: jwt的簽發時間
                .setSubject(userId + "-" + jwtVersion)        //sub(Subject):代表這個JWT的主體,即它的所有人,這個是一個json格式的字符串,可以存放什么userid,roldid之類的,作為什么用戶的唯一標志。
                .signWith(signatureAlgorithm, secretKey);//設置簽名使用的簽名算法和簽名使用的秘鑰
        //設置過期時間
        if (JWT_EXPIRE_TIME_LONG >= 0) {
            long expMillis = nowMillis + JWT_EXPIRE_TIME_LONG;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        String newToken = "jwt_" + builder.compact();
        return newToken;
    }

    /**
     * 生成新token時,初始化token
     * @param userId
     * @param newToken
     */
    public void initNewToken(String userId, String newToken){
        String token = redisTemplate.opsForValue().get("userJwtToken_"+userId);
        if(ToolUtil.isNotEmpty(token)){
            //老token設置過期時間 5分鍾
            redisTemplate.opsForValue().set(token, userId, OLD_TOKEN_EXPIRE_TIME, TimeUnit.MINUTES);
        }
        //新token初始化
        redisTemplate.opsForValue().set(newToken, userId);
        redisTemplate.opsForValue().set("userJwtToken_"+userId, newToken);
    }

    /**
     * 獲取jwt失效時間
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token).getExpiration();
    }

    /**
     * 獲取jwt的payload部分
     */
    public Claims getClaimFromToken(String token) {
        SecretKey secretKey = createSecretKey();
        return Jwts.parser()   //得到DefaultJwtParser
                .setSigningKey(secretKey)  //設置簽名的秘鑰
                .parseClaimsJws(token.replace("jwt_", ""))
                .getBody();
    }

    // 簽名私鑰
    private SecretKey createSecretKey(){
        byte[] encodedKey = Base64.decodeBase64(signKey);//本地的密碼解碼
        SecretKey secretKey = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");// 根據給定的字節數組使用AES加密算法構造一個密鑰,使用 encodedKey中的始於且包含 0 到前 leng 個字節這是當然是所有。(后面的文章中馬上回推出講解Java加密和解密的一些算法)
        return secretKey;
    }

攔截器

//判斷過期token是否合法
        String userId = jwtTokenUtil.checkExpireToken(token);
        try {
            if(ToolUtil.isEmpty(userId)) {
                userId = jwtTokenUtil.getUserFromToken(token);
            }

            if (userId != null) {
                String[] split = userId.split("-")[1].split(",");
                ArrayList<GrantedAuthority> authorities = new ArrayList<>();
                for (int i = 0; i < split.length; i++) {
                    authorities.add(new GrantedAuthorityImpl(split[i]));
                }

                //判斷token是否需要更新,返回當前可用token
                String newToken = jwtTokenUtil.getGoodToken(userId.split("-")[0]);
                //每次認證把uId塞入UserIdThreadLocal
                //這樣可以在當前請求線程里面 通過 UidHelper.get()獲取用戶id
                UidHelper.set(userId.split("-")[0]);
                response.setHeader("token", newToken);
                return new UsernamePasswordAuthenticationToken(userId, null, authorities);
            }
            logger.error("解析用戶ID為空,token:{}",token);
        } catch (Exception e) {
            logger.error("Token已過期,token:{}",token);
        }

 


免責聲明!

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



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