JWT (JSON Web Token) 可以用於儲存不敏感的訊息來讓 client 和 server 端做溝通,
其支援不同的簽名加密演算法,例如對稱加密的
HS256 或 非對稱加密的 RS256 等,
在這篇文章裡,我記錄了一下使用了 RS256
的 JWT 驗證過程。
RS256 結合了 RSASSA-PKCS1-v1_5 和 SHA-256,
RSASSA-PKCS1-v1_5
是一種非對稱加密演算法、
而 SHA-256 是一種雜湊演算法,參考 Digital Signature with RSASSA-PKCS1-v1_5。
RS256 跟 HS256 不同的地方是,RS256使用了非對稱加密的RSA演算法,所以擁有一對
Public
Key (公鑰) 和 Private Key (私錀),而 HS256 只有一把鑰匙。
在安全上,HS256
需要發佈方(例如提供 API 的 server 端)和驗證方(例如呼叫 API 的 client
端)都要擁有同一把錀匙,如果這把錀匙流露出去讓不法的第三方得知,
第三方將可使用這把錀匙偽造出假訊息的
JWT token 讓 server, client 端無法查覺。
跟只使用了一個密碼的 HS256 不同,RS256 使用了一對公錀與密錀,
提供 API 的
server 方可以以不公開的密錀對 JWT 做加密雜溱演算,
client 端及 server 端都可以用 server
公開出來的公錀來驗證 JWT 的合法性,能確認 JWT 確實為
server
端所發出且沒有被遭到竄改
Note:
https://jwt.io/ 是一個提供線上 JWT token
產生、驗證範例的網站,想要做測試時還蠻好用的,其也提供了不同的簽名演算法可供選擇。
在這篇文中,我示範了:
- 如何產生 RSA 的公錀及私錀。
- 如何使用 RSA 的私錀進行 RS256 JWT 的簽發。
- 如何使用 RSA 的公錀進行 RS256 JWT 的驗證。
首先是有用到的 library dependency,
pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>test</groupId> <artifactId>jwtTest</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> </project>
JwtTest.java:
package main; import java.io.UnsupportedEncodingException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.text.SimpleDateFormat; import java.util.Base64; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jws; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultClaims; public class JwtTest { public static void main(String[] args) throws Exception { KeyPair keyPair = generateRsaKeyPair(); String publicKeyStr = "-----BEGIN PUBLIC KEY-----" + new String(Base64.getEncoder().encode(keyPair.getPublic().getEncoded()), "UTF-8") + "-----END PUBLIC KEY-----"; String privateKeyStr = "-----BEGIN PRIVATE KEY-----" + new String(Base64.getEncoder().encode(keyPair.getPrivate().getEncoded()), "UTF-8") + "-----END PRIVATE KEY-----"; System.out.println("Public Key: " + publicKeyStr); System.out.println("Private Key: " + privateKeyStr); String someProperyValue = "someName"; SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); Claims claims = new DefaultClaims(); claims.setIssuer("issuer") .setSubject("test") .setExpiration(sdf.parse("2200-01-01")) .put("name", someProperyValue); String generatedRsaJwt = generateRsaJwt(claims, privateKeyStr); Claims parsedClaims = parseRsaJwt(generatedRsaJwt, publicKeyStr); System.out.println("JWT Token: " + generatedRsaJwt); //verify content System.out.println(someProperyValue.equals(parsedClaims.get("name", String.class))); //true } static String generateRsaJwt(Claims claims, String privateKeyStr) throws InvalidKeySpecException, UnsupportedEncodingException, NoSuchAlgorithmException { String generatedRsaJwt = null; privateKeyStr = privateKeyStr.replace("-----BEGIN PRIVATE KEY-----", ""); privateKeyStr = privateKeyStr.replace("-----END PRIVATE KEY-----", ""); privateKeyStr = privateKeyStr.replaceAll("\r\n", ""); privateKeyStr = privateKeyStr.replaceAll("\\s+", ""); PKCS8EncodedKeySpec keySpec_private = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKeyStr.getBytes("UTF-8"))); KeyFactory keyFactory_private = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory_private.generatePrivate(keySpec_private); generatedRsaJwt = Jwts.builder() .setHeaderParam("typ", "JWT") .setClaims(claims) .signWith(SignatureAlgorithm.RS256, privateKey) .compact(); return generatedRsaJwt; } static Claims parseRsaJwt(String jwtToParse, String publicKeyStr) throws NoSuchAlgorithmException, InvalidKeySpecException, UnsupportedEncodingException { Claims parsedRsaJwtClaims = null; publicKeyStr = publicKeyStr.replace("-----BEGIN PUBLIC KEY-----", ""); publicKeyStr = publicKeyStr.replace("-----END PUBLIC KEY-----", ""); publicKeyStr = publicKeyStr.replaceAll("\r\n", ""); publicKeyStr = publicKeyStr.replaceAll("\\s+", ""); X509EncodedKeySpec keySpec_public = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr.getBytes("UTF-8"))); KeyFactory keyFactory_public = KeyFactory.getInstance("RSA"); PublicKey publicKey_public = keyFactory_public.generatePublic(keySpec_public); Jws<Claims> jws = Jwts.parser() .setSigningKey(publicKey_public) .parseClaimsJws(jwtToParse); parsedRsaJwtClaims = jws.getBody(); return parsedRsaJwtClaims; } static KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException, UnsupportedEncodingException { KeyPair keyPair = null; SecureRandom secureRandom = new SecureRandom(); secureRandom.setSeed("test".getBytes("UTF-8")); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048, secureRandom); // secureRandom is not necessary, // you can also use keyPairGenerator.initialize(2048); for random seed // note: if use same seed for secureRandom, will get same public key and private key. keyPair = keyPairGenerator.generateKeyPair(); return keyPair; } }
說明:
- generateRsaKeyPair() 會產生一對 RSA Public Key 及 Private Key,SecureRandom 並非必須,只是做個示範,要注意的是 SecureRandom 如果給予一樣的種子 (Seed) ,可能會造成每次產生出一樣的公錀及密錀 (但也有可能不會,看SecureRandom、KeyGenerator 等底層演算法而定)。
- generateRsaJwt() 使用私錀為 JWT 做簽章,parseRsaJwt() 使用公錀對 JWT 做驗證,如果有驗證問題的話,例如有人用了另一個錯吳的私錀 (真正的私錀不可流露出去,理論上其他第三方應無法知道真正的私錀) 做假訊息的簽章,在用公錀驗證時就會出現驗證錯的訊息:
Exception in thread "main" io.jsonwebtoken.SignatureException: JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.
參考資料: