JWT(二):使用 Java 實現 JWT


JWT(一):認識 JSON WebToken
JWT(二):使用 Java 實現 JWT

介紹

原理在上篇《JWT(一):認識 JSON Web Token》已經說過了,實現起來並不難,你可以自己寫一個 jwt 工具類(如果你有興趣的話)

當然了,重復造輪子不是程序員的風格,我們主張拿來主義!
魯迅-又拿我說事兒.jpg

JWT 官網提供了多種語言的 JWT 庫,詳情可以參考 https://jwt.io/#debugger 頁面下半部分

建議使用 jjwt庫 ,它的github地址 https://github.com/jwtk/jjwt

jjwt 版本 0.10.7,它和 0.9.x 有很大的區別,一定要注意!!!

本文分5部分

  • 第1部分:以簡單例子演示生成、驗證、解析 jwt 過程
  • 第2部分:介紹 jjwt 的常用方法
  • 第3部分:封裝一個常用的 jwt 工具類
    如果只是拿來主義,看到這里就可以了
  • 第4部分:介紹 jjwt 的各種簽名算法
  • 第5部分:對 jwt 進行安全加密

簡單例子

引入 MAVN 依賴

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>

一個例子

    
    // 生成密鑰
    String key = "0123456789_0123456789_0123456789";
    SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());

    // 1. 生成 token
    String token = Jwts.builder()     // 創建 JWT 對象
            .setSubject("JSON Web Token")   // 設置主題(聲明信息)
            .signWith(secretKey)    // 設置安全密鑰(生成簽名所需的密鑰和算法)
            .compact(); // 生成token(1.編碼 Header 和 Payload 2.生成簽名 3.拼接字符串)
    System.out.println(token);

    //token = token + "s";

    // 2. 驗證token,如果驗證token失敗則會拋出異常
    try {
        Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token);
        // OK, we can trust this token
        System.out.println("驗證成功");
    } catch (JwtException e) {
        //don't trust the token!
        System.out.println("驗證失敗");
    }

    // 3. 解析token
    Claims body = Jwts.parser()     // 創建解析對象
            .setSigningKey(secretKey)   // 設置安全密鑰(生成簽名所需的密鑰和算法)
            .parseClaimsJws(token)  // 解析token
            .getBody(); // 獲取 payload 部分內容
    System.out.println(body);

輸出結果:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKU09OIFdlYiBUb2tlbiJ9.QwmY_0qXW4BhAHcDpxz62v3xqkFYbg5lsZQhM2t-kVs
驗證成功
{sub=JSON Web Token}

常用方法

以下內容建議參考源碼獲知更多詳情

Jwts.builder() 創建了 DefaultJwtBuilder 對象,該對象的常用方法如下:

compact() 方法中會自動根據簽名算法設置頭部信息,當然也可以手動設置

  • setHeader(Header header): JwtBuilder
  • setHeader(Map<String, Object> header): JwtBuilder
  • setHeaderParams(Map<String, Object> params): JwtBuilder
  • setHeaderParam(String name, Object value): JwtBuilder

參數 Header 對象 可通過 Jwts.header(); 創建,它簡單得就像一個 map (把它當做 map 使用即可)

Payload

至少設置一個 claims,否則在生成簽名時會拋出異常

  • setClaims(Claims claims): JwtBuilder
  • setClaims(Map<String, Object> claims): JwtBuilder
  • addClaims(Map<String, Object> claims): JwtBuilder
  • setIssuer(String iss): JwtBuilder
  • setSubject(String sub): JwtBuilder
  • setAudience(String aud): JwtBuilder
  • setExpiration(Date exp): JwtBuilder
  • setNotBefore(Date nbf): JwtBuilder
  • setIssuedAt(Date iat): JwtBuilder
  • setId(String jti): JwtBuilder
  • claim(String name, Object value: JwtBuilder

參數對象 Claims 同 Header 類似,通過 Jwts.claims() 創建,同樣簡單得就像一個 map

值得注意的一點是:不要在 setXxx 之后調用 setClaims(Claims claims) 或 setClaims(Map<String, Object> claims),因為這兩個方法會覆蓋所有已設置的 claim

Signature

  • signWith(Key key)
  • signWith(Key key, SignatureAlgorithm alg)
  • signWith(SignatureAlgorithm alg, byte[] secretKeyBytes)
  • signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
  • signWith(SignatureAlgorithm alg, Key key)

以上方法最終就是設置兩個對象:key 和 algorithm,分別代表密鑰和算法
方法內部生成密鑰使用的方法的和演示中的一樣

SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());

注意:key 的長度必須符合簽名算法的要求(避免生成弱密鑰)
HS256:bit 長度要>=256,即字節長度>=32
HS384:bit 長度要>=384,即字節長度>=48
HS512:bit 長度要>=512,即字節長度>=64
在 secret key algorithms 名稱中的數字代表了最小bit長度

更多簽名算法的詳情,請參考簽名算法小節

封裝 JWT 工具類

package com.liuchuanv.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

/**
 * JSON Web Token 工具類
 *
 * @author LiuChuanWei
 * @date 2019-12-11
 */
public class JwtUtils {

    /**
     * key(按照簽名算法的字節長度設置key)
     */
    private final static String SECRET_KEY = "0123456789_0123456789_0123456789";
    /**
     * 過期時間(毫秒單位)
     */
    private final static long TOKEN_EXPIRE_MILLIS = 1000 * 60 * 60;

    /**
     * 創建token
     * @param claimMap
     * @return
     */
    public static String createToken(Map<String, Object> claimMap) {
        long currentTimeMillis = System.currentTimeMillis();
        return Jwts.builder()
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date(currentTimeMillis))    // 設置簽發時間
                .setExpiration(new Date(currentTimeMillis + TOKEN_EXPIRE_MILLIS))   // 設置過期時間
                .addClaims(claimMap)
                .signWith(generateKey())
                .compact();
    }

    /**
     * 驗證token
     * @param token
     * @return 0 驗證成功,1、2、3、4、5 驗證失敗
     */
    public static int verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(generateKey()).parseClaimsJws(token);
            return 0;
        } catch (ExpiredJwtException e) {
            e.printStackTrace();
            return 1;
        } catch (UnsupportedJwtException e) {
            e.printStackTrace();
            return 2;
        } catch (MalformedJwtException e) {
            e.printStackTrace();
            return 3;
        } catch (SignatureException e) {
            e.printStackTrace();
            return 4;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            return 5;
        }
    }

    /**
     * 解析token
     * @param token
     * @return
     */
    public static Map<String, Object> parseToken(String token) {
        return Jwts.parser()  // 得到DefaultJwtParser
                .setSigningKey(generateKey()) // 設置簽名密鑰
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 生成安全密鑰
     * @return
     */
    public static Key generateKey() {
       return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
    }
}

測試代碼如下:

  //Map<String, Object> map = new HashMap<String, Object>();
        //map.put("userId", 1002);
        //map.put("userName", "張曉明");
        //map.put("age", 12);
        //map.put("address", "山東省青島市李滄區");
        //String token = JwtUtils.createToken(map);
        //System.out.println(token);

        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ZWM2NWNhNC0wZjVmLTRlOTktOTI5NS1mYWUyN2UwODIzYzQiLCJpYXQiOjE1NzY0OTI4NjYsImV4cCI6MTU3NjQ5NjQ2NiwiYWRkcmVzcyI6IuWxseS4nOecgemdkuWym-W4guadjuayp-WMuiIsInVzZXJOYW1lIjoi5byg5pmT5piOIiwidXNlcklkIjoxMDAyLCJhZ2UiOjEyfQ.6Z18aIA6y52ntQkV3BwlYiVK3hL3R2WFujjTmuvimww";
        int result = JwtUtils.verifyToken(token);
        System.out.println(result);

        Map<String, Object> map = JwtUtils.parseToken(token);
        System.out.println(map);

輸出結果:

0
{jti=4ec65ca4-0f5f-4e99-9295-fae27e0823c4, iat=1576492866, exp=1576496466, address=山東省青島市李滄區, userName=張曉明, userId=1002, age=12}

簽名算法

12 種簽名算法

JWT 規范定義了12種標准簽名算法:3種 secret key 算法和9種非對稱密鑰算法

  • HS256: HMAC using SHA-256
  • HS384: HMAC using SHA-384
  • HS512: HMAC using SHA-512
  • ES256: ECDSA using P-256 and SHA-256
  • ES384: ECDSA using P-384 and SHA-384
  • ES512: ECDSA using P-521 and SHA-512
  • RS256: RSASSA-PKCS-v1_5 using SHA-256
  • RS384: RSASSA-PKCS-v1_5 using SHA-384
  • RS512: RSASSA-PKCS-v1_5 using SHA-512
  • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
  • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
  • PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

根據算法名稱可分為四類:HSxxx(secret key 算法)、ESxxx、RSxxx、PSxxx

HSxxx、ESxxx 中的 xxx 表示算法 key 最小 Bit 長度
RSxxx、PSxxx 中的 xxx 表示算法 key 最小 Byte 長度

規定key的最小長度是為了避免因 key 過短生成弱密鑰

生成密鑰

jjwt 生成 secret key 兩種方法

String key = "1234567890_1234567890_1234567890";
// 1. 根據key生成密鑰(會根據字節參數長度自動選擇相應的 HMAC 算法)
SecretKey secretKey1 = Keys.hmacShaKeyFor(key.getBytes());
// 2. 根據隨機數生成密鑰
SecretKey secretKey2 = Keys.secretKeyFor(SignatureAlgorithm.HS256);
  • 方法 Keys.hmacShaKeyFor(byte[]) 內部也是 new SecretKeySpec(bytes, alg.getJcaName()) 來生成密鑰的
  • 方法 Keys.secretKeyFor(SignatureAlgorithm) 內部使用 KeyGenerator.generateKey() 生成密鑰

jjwt 也提供了非對稱密鑰對的生成方法

// 1. 使用jjwt提供的方法生成
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);    //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512

// 2. 手動生成
int keySize = 1024;
// RSA算法要求有一個可信任的隨機數源
SecureRandom secureRandom = new SecureRandom();
// 為RSA算法創建一個KeyPairGenerator對象 
KeyPairGenerator keyPairGenerator = null;
try {
    keyPairGenerator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}
// 利用上面的隨機數據源初始化這個KeyPairGenerator對象
keyPairGenerator.initialize(keySize, secureRandom);
// 生成密鑰對
KeyPair keyPair2 = keyPairGenerator.generateKeyPair();

  • Keys.keyPairFor(SignatureAlgorithm) 會根據算法自動生成相應長度的
  • signWith(secretKey) 會根據密鑰長度自動選擇相應算法,也可以指定任意算法(指定的算法不受密鑰長度限制,可任意選擇,即用 RS256生成的密鑰,可以 signWith(secretKey, SignatureAlgorithm.RS512),但是 JJWT 並不建議這么做)
  • 在加密時使用 keyPair.getPrivate() ,解密時使用 keyPair.getPublic()

不同密鑰生成token

以上都是使用同一密鑰簽名生成所有的token,下面我們使用不同的密鑰

這一個特性可以應用於不同用戶/角色使用不同的密鑰生成的 token,幫助你更好的構建權限系統

  1. 首先在 Header(或 claims)中設置一個 keyId

  2. 定義一個類,繼承 SigningKeyResolverAdapter,並重寫 resolveSigningKey() 或 resolveSigningKeyBytes() 方法

    public class MySigningKeyResolver extends SigningKeyResolverAdapter {
        @Override
        public Key resolveSigningKey(JwsHeader header, Claims claims) {
            // 除了從 header 中獲取 keyId 外,也可以從 claims 中獲取(前提是在 claims 中設置了 keyId 聲明)
            String keyId = header.getKeyId();
            // 根據 keyId 查找相應的 key
            Key key = lookupVerificationKey(keyId);
            return key;
        }
    
        public Key lookupVerificationKey(String keyId) {
            // TODO 根據 keyId 獲取 key,比如從數據庫中獲取
            // 下面語句僅做演示用,絕對不可用於實際開發中!!!
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
            return Keys.hmacShaKeyFor(key.getBytes());
        }
    }
    
  3. 解析時,不再調用 setSigningKey(SecretKey) ,而是調用 setSigningKeyResolver(SigningKeyResolver)

    // 生成密鑰
            // TODO 此處 keyId 僅做演示用,實際開發中可以使用 UserId、RoleId 等作為 keyId
            String keyId = new Long(System.currentTimeMillis()).toString();
            System.out.println("keyId=" + keyId);
    
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
            SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());
    
            // 1. 生成 token
            String token = Jwts.builder()
                    .setHeaderParam(JwsHeader.KEY_ID, keyId)    // 設置 keyId(當然也可以在 claims 中設置)
                    .setSubject("JSON Web Token")
                    .signWith(secretKey)
                    .compact();
            System.out.println("token=" + token);
    
            // 2. 驗證token
            // token 使用了不同的密鑰生成簽名,在解析時就不用調用 setSigningKey(SecretKey) 了
            // 而是調用 setSigningKeyResolver(SigningKeyResolver)
            try {
                Jwts.parser()
                        .setSigningKeyResolver(new MySigningKeyResolver())
                        .parseClaimsJws(token);
                // OK, we can trust this token
                System.out.println("token驗證成功");
            } catch (JwtException e) {
                //don't trust the token!
                System.out.println("token驗證失敗");
            }
    
    

安全加密

敬請期待 .....


免責聲明!

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



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