操作 JWT:nimbus-jose-jwt 庫
nimbus-jose-jwt、jose4j、java-jwt 和 jjwt 是幾個 Java 中常見的操作 JWT 的庫。就使用細節而言,nimbus-jos-jwt(和jose4j)要好於 java-jwt 和 jjwt 。
nimbus-jose-jwt 官網(opens new window)
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.11.1</version> </dependency>
#1. 相關概念
#JWT 和 JWS
這里我們需要了解下 JWT、JWS、JWE 三者之間的關系:
-
JWT(JSON Web Token)指的是一種規范,這種規范允許我們使用 JWT 在兩個組織之間傳遞安全可靠的信息。
-
JWS(JSON Web Signature)和 JWE(JSON Web Encryption)是 JWT 規范的兩種不同實現,我們平時最常使用的實現就是 JWS 。
簡單來說,JWT 和 JWS、JWE 類似於接口與實現類。由於,我們使用的是 JWS ,所以,后續內容中,就直接列舉 JWS 相關類,不再細分 JWS 和 JWE 了,numbus-jose-jwt 中的 JWE 相關類和接口我們也不會使用到。
#加密算法
另外,還有一對可能會涉及的概念:對稱加密和非對稱加密:
-
『對稱加密』指的是使用相同的秘鑰來進行加密和解密,如果你的秘鑰不想暴露給解密方,考慮使用非對稱加密。在加密方和解密方是同一個人(或利益關系緊密)的情況下可以使用它。
-
『非對稱加密』指的是使用公鑰和私鑰來進行加密解密操作。對於加密操作,公鑰負責加密,私鑰負責解密,對於簽名操作,私鑰負責簽名,公鑰負責驗證。非對稱加密在 JWT 中的使用顯然屬於簽名操作。在加密方和解密方是不同人(或不同利益方)的情況下可以使用它。
nimbus-jose-jwt 支持的算法都在它的 JWSAlgorithm 和 JWEAlgorithm 類中有定義。
例如:JWSAlgorithm algorithm = JWSAlgorithm.HS256
#2. 核心 API 介紹
#加密過程
-
在 nimbus-jose-jwt 中,使用 Header 類代表 JWT 的頭部,不過,Header 類是一個抽象類,我們使用的是它的子類 JWSHeader 。
創建頭部對象:
JWSHeader jwsHeader = new JWSHeader.Builder(algorithm) // 加密算法 .type(JOSEObjectType.JWT) // 靜態常量 .build();
Copied!另外,你可以通過
.getParsedBase64URL()
方法求得頭部信息的 Base64 形式(這也是 JWT 中的實際頭部信息):header.getParsedBase64URL();
Copied! -
使用 Payload 類的代表 JWT 的荷載部分,
創建荷載部對象
Payload payload = new Payload("hello world"); // 這里還可以傳 JSON 串,或 Map 。
Copied!另外,你可以通過
.toBase64URL()
方法求得荷載部信息的 Base64 形式(這也是 JWT 中的實際荷載部信息):payload.toBase64URL();
Copied! -
簽名部分沒有專門的類表示,只有通用類 Base64URL ,而且簽名部分並非你自己創建出來的,而是靠
頭部 + 荷載部 + 加密算法
算出來的。在 nimbus-jose-jwt 中,簽名算法由 JWSAlgorithm 表示。
注意
在創建 JWSHeader 對象時就需要指定簽名算法,因為在標准中,頭部需要保存簽名算法名字。
用頭部和荷載部分,再加上指定的簽名算法和密鑰來生成簽名部分的過程,在 nimbus-jose-jwt 中被稱為『簽名(sign)』。
nimbus-jose-jwt 專門提供了一個簽名器 JWSSigner ,用來參與到簽名過程中。密鑰就是在創建簽名器的時候指定的:
JWSSigner jwsSigner = new MACSigner(secret);
Copied! -
最終,整個 JWT 由一個 JWSObject 對象表示:
JWSObject jwsObject = new JWSObject(jwsHeader, payload); // 進行簽名(根據前兩部分生成第三部分) jwsObject.sign(jwsSigner);
Copied!在 nimbus-jose-jwt 中 JWSObject 是有狀態的:未簽名、已簽名和簽名中。很顯然,在執行外
.sign()
方法之后,JWSObject 對象就變成了已簽名狀態。當然,我們最終『要』的是 JWT 字符串,而不是對象,這里接着對代表 JWT 的 JWSObject 對象調用
.serialize()
方法即可:String token = jwsObject.serialize();
Copied!
#解密
反向的解密和驗證過程核心 API 就 2 個:JWSObject 的靜態方法 parse 方法和驗證其 JWSVerifier 對象。
JWSObject.parse()
方法是上面的 serialize 方法的反向操作,它可以通過一個 JWT 串生成 JWSObject 。有了 JWObject 之后,你就可以獲得 header 和 payload 部分了。
如果你想直接驗證 JWSObject 對象的合法性,你需要創建一個 JWSVerifier 對象。
JWSVerifier jwsVerifier = new MACVerifier(secret);
然后直接調用 jwsObject 對象的 verify 方法:
if (!jwsObject.verify(jwsVerifier)) { throw new RuntimeException("token 簽名不合法!"); }
#官網的 HS256 示例
import java.security.SecureRandom; import com.nimbusds.jose.*; import com.nimbusds.jose.crypto.*; // Generate random 256-bit (32-byte) shared secret SecureRandom random = new SecureRandom(); byte[] sharedSecret = new byte[32]; random.nextBytes(sharedSecret); // Create HMAC signer JWSSigner signer = new MACSigner(sharedSecret); // Prepare JWS object with "Hello, world!" payload JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload("Hello, world!")); // Apply the HMAC jwsObject.sign(signer); // To serialize to compact form, produces something like // eyJhbGciOiJIUzI1NiJ9.SGVsbG8sIHdvcmxkIQ.onO9Ihudz3WkiauDO2Uhyuz0Y18UASXlSc1eS0NkWyA String s = jwsObject.serialize(); // To parse the JWS and verify it, e.g. on client-side jwsObject = JWSObject.parse(s); JWSVerifier verifier = new MACVerifier(sharedSecret); assertTrue(jwsObject.verify(verifier)); assertEquals("Hello, world!", jwsObject.getPayload().toString());
#在 Payload 中存對象
在上例(和官方示例中)payload 中存放的是簡單的字符串,其實,更方便更有使用價值的是存入一個 json 串。所以,我們可以自定義專本用於存入 payload 中的 javabean,例如:
@Data @NoArgsConstructor @AllArgsConstructor @Builder public class Claims { // "主題" private String sub; // "簽發時間" private Long iat; // 過期時間 private Long exp; // JWT的ID private String jti; // "用戶名稱" private String username; // "用戶擁有的權限" //private List<String> authorities; }
這樣在創建 Payload 時,需要多一步轉換操作:
ObjectMapper mapper = new ObjectMapper(); // 這里使用的是 Jackson 庫 // 將負載信息封裝到Payload中 Payload payload = new Payload(mapper.writeValueAsString(claims));
反向的取出內容時,也是一樣的道理。
#非對稱加密(RSA)
上面,我們使用的是對稱加密算法。而非對稱加密指的是分別使用『公鑰』和『私鑰』來進行加密、解密操作。私鑰負責加密,負責生成 JWT 的簽名部分;公鑰負責解密,負責驗證 JWT 是否是偽造的。
要使用 RSA ,我們需要生成一個『證書文件』,這里將使用 Java 自帶的 keytool
工具來生成 jks 證書文件,該工具在 JDK 的 bin 目錄下。
打開 CMD 命令界面,使用如下命令生成證書文件,設置別名為 jwt ,文件名為 jwt.jks
:
語法規則:
keytool -genkey -alias <證書別名> -keyalg <密鑰算法> -keystore <證書庫的位置和名稱> -keysize <密鑰長度> -validity <證書有效期(天數)>
例子:
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
3 點注意事項
- 有可能你會遇到
keytool 錯誤: java.io.FileNotFoundException: jwt.jks (拒絕訪問。)
問題。以防萬一使用管理員身份啟動 CMD 命令行。 - 生成的 jwt.jks 文件在你命令行的當前目錄下,請務必知道你自己在哪,別找不到生成的 jwt.jks 文件。
- 在開發、演示過程中,生成 jwt.jks 時所使用的密碼盡量簡單易記,以免自己忘記了。
你會看到類似如下內容:
D:\>keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
輸入密鑰庫口令:
再次輸入新口令:
您的名字與姓氏是什么?
[Unknown]:
您的組織單位名稱是什么?
[Unknown]:
您的組織名稱是什么?
[Unknown]:
您所在的城市或區域名稱是什么?
[Unknown]:
您所在的省/市/自治區名稱是什么?
[Unknown]:
該單位的雙字母國家/地區代碼是什么?
[Unknown]:
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正確?
[否]: y
輸入 <jwt> 的密鑰口令
(如果和密鑰庫口令相同, 按回車):
再次輸入新口令:
Warning:
JKS 密鑰庫使用專用格式。建議使用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 遷移到行業標准格式 PKCS12。
將證書文件 jwt.jks
復制到項目的 resource 目錄下,然后需要從證書文件中讀取 RSAKey ,這里我們需要在 pom.xml 中添加一個 Spring Security 的 RSA 依賴;
<!-- Spring Security RSA 含有相關工具類 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-rsa</artifactId> <!-- spring-cloud-commons-dependencies 已含有版本信息 --> </dependency>
關於引入 spring-security-rsa 包
其實,我們引入 spring-security-rsa 是因為我們要用到它里面的一個名為 KeyStoreKeyFactory 的工具類。考慮到 KeyStoreKeyFactory 工具類也沒有引來 spring-security-rsa 中的其它的任何東西,所以,我們也可以把 KeyStoreKeyFactory 單獨地摘出來。
-
從
jwt.jks
文件生成 RSAKey 對象:public RSAKey generateRsaKey() { // 從 classpath 下獲取 RSA 秘鑰對 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); // 獲取 RSA 公鑰 RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 獲取 RSA 私鑰 RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build(); return rsaKey; }
Copied! -
根據 RSAKey 對象生成 JWT/JWS 字符串:
RSAKey rsaKey = generateRsaKey(); // JWS 頭 JWSHeader jwsHeader = new JWSHeader .Builder(JWSAlgorithm.RS256) // 指定 RSA 算法 .type(JOSEObjectType.JWT) .build(); // JWS 荷載 Payload payload = new Payload("hello world"); // JWS 簽名 JWSObject jwsObject = new JWSObject(jwsHeader, payload); JWSSigner jwsSigner = new RSASSASigner(rsaKey, true); // rsaKey 生成簽名器 jwsObject.sign(jwsSigner); // JWT/JWS 字符串 String jwt = jwsObject.serialize(); System.out.println(jwt);
Copied! -
根據 RSAKey 對象(的公鑰)解析 JWT/JWS 字符串:
// JWT/JWS 字符串轉 JWSObject 對象 String token = "..."; JWSObject jwsObject = JWSObject.parse(token); // 根據公要生成驗證器 RSAKey rsaKey = generateRsaKey(); RSAKey publicRsaKey = rsaKey.toPublicJWK(); System.out.println(publicRsaKey); // show 公鑰 JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey); // 使用校驗器校驗 JWSObject 對象的合法性 if (!jwsObject.verify(jwsVerifier)) { throw new RuntimeException("token簽名不合法!"); } // 拆解 JWT/JWS,獲得荷載中的內容 String payload = jwsObject.getPayload().toString(); System.out.println(payload); // show 荷載