- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt-api</artifactId>
- <version>0.11.2</version>
- </dependency>
- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt-impl</artifactId>
- <version>0.11.2</version>
- <scope>runtime</scope>
- </dependency>
- <dependency>
- <groupId>io.jsonwebtoken</groupId>
- <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
- <version>0.11.2</version>
- <scope>runtime</scope>
- </dependency>
- <!-- Uncomment this next dependency if you are using JDK 10 or earlier and you also want to use
- RSASSA-PSS (PS256, PS384, PS512) algorithms. JDK 11 or later does not require it for those algorithms:
- <dependency>
- <groupId>org.bouncycastle</groupId>
- <artifactId>bcprov-jdk15on</artifactId>
- <version>1.60</version>
- <scope>runtime</scope>
- </dependency>
- -->
- 复制代码
- import io.jsonwebtoken.Jwts;
- import io.jsonwebtoken.SignatureAlgorithm;
- import io.jsonwebtoken.security.Keys;
- import java.security.Key;
-
- //创建一个签名密钥,通常在配置文件中设置
- Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
- String jws = Jwts.builder().setSubject("Joe").signWith(key).compact();
- 复制代码
输出如下: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
通常我们需要对拿到的JWS进行验证,以丢弃错误的JWS。
- assert Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws)
- .getBody().getSubject().equals("Joe");
- 复制代码
如果解析错误,可通过接收异常进行判断:
- try {
- Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compactJws);
- //OK, we can trust this JWT
- } catch (JwtException e) {
- //don't trust the JWT!
- }
- 复制代码
签名首先保证jwts是正确的,然后保证没有被篡改
- // header
- {
- "alg": "HS256"
- }
-
- //body
- {
- "sub": "Joe"
- }
- 复制代码
- String header = '{"alg":"HS256"}'
- String claims = '{"sub":"Joe"}'
- 复制代码
- String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
- String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
- 复制代码
- String concatenated = encodedHeader + '.' + encodedClaims
- 复制代码
- Key key = getMySecretKey()
- byte[] signature = hmacSha256( concatenated, key )
- 复制代码
- String jws = concatenated + '.' + base64URLEncode( signature )
- 复制代码
eyJhbGciOiJIUzI1NiJ9``.``eyJzdWIiOiJKb2UifQ``.``1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
实际上没有这么复杂,这里只是说明一个过程,JJWT提供了更简单的API来操作这一切。
JJWT提供了12种签名密钥,其中3种对称加密和9种不对称加密:
可以通过枚举来使用:io.jsonwebtoken.SignatureAlgorithm
可以使用安全工具类:io.jsonwebtoken.security.Keys
比如使用HS256签名密钥:
- SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); //or HS384 or HS512
- // 如果想保存该密钥,需要获取字符串表示:
- String secretString = Encoders.BASE64.encode(key.getEncoded());
- 复制代码
比如使用非对称加密:
- //还可以使用 RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
- KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);
- 复制代码
您可以使用私钥(keyPair.getPrivate())来创建JWS,并使用公钥(keyPair.getPublic())来解析/验证JWS
- String jws = Jwts.builder() // 使用Jwts.builder()方法创建一个JwtBuilder实例
- .setSubject("Bob") // 调用JwtBuilder方法,根据需要添加头参数和声明
- .signWith(key) // 指定想要用来为JWT签名的SecretKey或非对称PrivateKey
- .compact(); // 最后,调用compact()方法进行压缩和签名,生成最终的jws
- 复制代码
JWT Header提供了与JWT的Claims相关的内容、格式和加密操作的元数据。
- String jws = Jwts.builder()
- .setHeaderParam("kid", "myKeyId")
- // ... etc ...
- 复制代码
每次setHeaderParam被调用时,它只是简单地将键值对附加到一个内部的Header实例,可能会覆盖任何现有的同名键值对。
注意:您不需要设置alg或zip报头参数,因为JJWT会根据使用的签名算法或压缩算法自动设置它们。
- Header header = Jwts.header();
- populate(header); //implement me
- String jws = Jwts.builder().setHeader(header)
- // ... etc ...
- 复制代码
注意:调用setHeader将覆盖任何现有的头的名称/值对,这些名称可能已经被设置。然而,在所有情况下,JJWT仍然会设置(并覆盖)任何alg和zip头文件,无论它们是否在指定的头文件对象中。
- Map<String,Object> header = getMyHeaderMap(); //implement me
- String jws = Jwts.builder().setHeader(header)
- // ... etc ...
- 复制代码
注意:调用setHeader将覆盖任何现有的头的名称/值对,这些名称可能已经被设置。然而,在所有情况下,JJWT仍然会设置(并覆盖)任何alg和zip头文件,无论它们是否在指定的头文件对象中。
claims是JWT的“主体”,包含JWT创建者希望呈现给JWT接收者的信息。
JwtBuilder为JWT规范中定义的标准注册Claim名称提供了方便的setter方法。它们是:
例如:
- String jws = Jwts.builder()
- .setIssuer("me")
- .setSubject("Bob")
- .setAudience("you")
- .setExpiration(expiration) //a java.util.Date,如System.currentTimeMillis()+long
- .setNotBefore(notBefore) //a java.util.Date
- .setIssuedAt(new Date()) // for example, now
- .setId(UUID.randomUUID()) //just an example id
- /// ... etc ...
- 复制代码
如果你需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,你可以根据需要简单地调用JwtBuilder声明一次或多次:
- String jws = Jwts.builder()
- .claim("hello", "world")
- // ... etc ...
- 复制代码
每次调用claim时,它只是将键-值对附加到内部的Claims实例,可能会覆盖任何现有的同名键/值对。
显然,您不需要为任何标准的claim名称调用claim,建议相反调用标准的setter方法,这样可以增强可读性。
- Claims claims = Jwts.claims();
- populate(claims); //implement me
- String jws = Jwts.builder()
- .setClaims(claims)
- // ... etc ...
- 复制代码
注意:调用setClaims将用可能已经设置的相同名称覆盖任何现有的索赔名称/值对。
- Map<String,Object> claims = getMyClaimsMap(); //implement me
- String jws = Jwts.builder()
- .setClaims(claims)
- // ... etc ...
- 复制代码
注意:调用setClaims将用可能已经设置的相同名称覆盖任何现有的索赔名称/值对。
建议您通过调用JwtBuilder的signWith方法来指定签名密钥,并让JJWT决定指定密钥所允许的最安全的算法:
- String jws = Jwts.builder()
- // ... etc ...
- .signWith(key) // <---
- .compact();
- 复制代码
当使用signWith时,JJWT也会自动设置所需的alg头与相关的算法标识符。
类似地,如果你用一个4096位长的RSA PrivateKey调用signWith, JJWT将使用RS512算法并自动将alg头设置为RS512。
注意:你不能用publickey签名jwt,因为这总是不安全的。JJWT将用一个InvalidKeyException拒绝任何指定的PublicKey签名。
如果您希望使用HMAC-SHA算法对JWS进行签名,并且您有一个秘密密钥字符串或编码的字节数组,那么您将需要将其转换为一个SecretKey实例,以用作signWith方法参数。
在某些特定的情况下,您可能希望为给定的键覆盖JJWT的默认选择算法。
例如,如果你有一个2048位的RSA PrivateKey, JJWT会自动选择RS256算法。如果你想使用RS384或RS512,你可以用重载的signWith方法手动指定它,该方法接受signaturealalgorithm作为一个附加参数:
- .signWith(privateKey, SignatureAlgorithm.RS512) // <---
- .compact();
- 复制代码
这是允许的,因为JWT规范允许任何RSA算法强度对任何RSA密钥>= 2048位。JJWT更倾向于使用RS512表示键>= 4096位,其次是RS384表示键>= 3072位,最后是RS256表示键>= 2048位。
然而,在所有情况下,无论您选择的算法是什么,JJWT都会断言,根据JWT规范的要求,允许为该算法使用指定的键。
您可以如下方式读取(解析)JWS:
- Jws<Claims> jws;
- try {
- // 使用Jwts.parserBuilder()方法创建一个JwtParserBuilder实例
- jws = Jwts.parserBuilder()
- // 指定要用于验证JWS签名的SecretKey或非对称PublicKey
- .setSigningKey(key)
- // 调用JwtParserBuilder上的build()方法来返回一个线程安全的JwtParser
- .build()
- // 最后,使用jws String调用parseClaimsJws(String)方法,生成原始jws
- .parseClaimsJws(jwsString);
- // we can safely trust the JWT
-
- catch (JwtException ex) {
- // 在解析或签名验证失败的情况下,整个调用被包装在一个try/catch块中。
- // 我们将在后面讨论异常和失败的原因
- // we *cannot* use the JWT as intended by its creator
- }
- 复制代码
注意:如果您期待JWS,请始终调用JwtParser的parseClaimsJws方法(而不是其他类似的可用方法),因为这保证了解析签名的jwt的正确的安全模型。
读取JWS时要做的最重要的事情是指定用于验证JWS的加密签名的密钥。如果签名验证失败,则不能安全信任JWT,应丢弃该JWT。
那么我们用哪个键来验证呢?
- Jwts.parserBuilder()
- .setSigningKey(secretKey) // <----
- .build()
- .parseClaimsJws(jwsString);
- 复制代码
- Jwts.parserBuilder()
- .setSigningKey(publicKey) // <---- publicKey, not privateKey
- .build()
- .parseClaimsJws(jwsString);
- 复制代码
但是您可能已经注意到—如果您的应用程序不只使用一个SecretKey或KeyPair怎么办?如果jws可以使用不同的秘钥或公钥/私钥,或两者的组合来创建呢?如果不能首先检查JWT,如何知道指定哪个键?
在这些情况下,你不能用一个键调用JwtParserBuilder的setSigningKey方法-相反,你将需要使用一个SigningKeyResolver,下面将介绍。
- SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
- Jwts.parserBuilder()
- .setSigningKeyResolver(signingKeyResolver) // <----
- .build()
- .parseClaimsJws(jwsString);
- 复制代码
- public class MySigningKeyResolver extends SigningKeyResolverAdapter {
- @Override
- public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
- // implement me
- }
- }
- 复制代码
JwtParser将在解析JWS JSON之后,但在验证JWS签名之前调用resolveSigningKey方法。这允许您检查jwheader和Claims参数,以获取任何可以帮助您查找用于验证特定jws的Key的信息。对于具有更复杂安全模型的应用程序来说,这是非常强大的,因为这些应用程序可能在不同的时间或不同的用户或客户使用不同的密钥。
您可能要检查哪些数据?
JWT规范支持的方法是在创建JWS时在JWS header中设置一个kid(Key ID),例如:
- Key signingKey = getSigningKey();
- String keyId = getKeyId(signingKey); //通过Key获取redis中的kid
- String jws = Jwts.builder()
- .setHeaderParam(JwsHeader.KEY_ID, keyId) // 1
- .signWith(signingKey) // 2
- .compact();
- 复制代码
然后在解析过程中,您的SigningKeyResolver可以检查JwsHeader以获取kid,然后使用该值从某处(如数据库)查找键。例如:
- public class MySigningKeyResolver extends SigningKeyResolverAdapter {
- @Override
- public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
- //获取kid
- String keyId = jwsHeader.getKeyId();
- Key key = lookupVerificationKey(keyId); //在redis中查找Key
- return key;
- }
- }
- 复制代码
注意:检查jwheader . getkeyid()只是查找键的最常见方法—您可以检查任意数量的报头字段或声明,以确定如何查找验证键。这一切都取决于JWS是如何创建的。
最后:对于HMAC算法,返回的验证密钥应该是一个SecretKey,而对于非对称算法,返回的密钥应该是一个PublicKey(而不是PrivateKey)。
可以强制要求JWS必须满足指定条件。
例如,假设您要求正在解析的JWS具有特定的sub(subject)值,否则您可能不信任令牌。你可以通过使用JwtParserBuilder上的各种require*方法之一来实现:
- try {
- Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
- } catch(InvalidClaimException ice) {
- // the sub field was missing or did not have a 'jsmith' value
- }
- 复制代码
你也可以使用require(fieldName, requiredFieldValue)方法来要求自定义字段,例如:
- try {
- Jwts.parserBuilder().require("myfield", "myRequiredValue").setSigningKey(key).build().parseClaimsJws(s);
- } catch(InvalidClaimException ice) {
- // the 'myfield' field was missing or did not have a 'myRequiredValue' value
- }
- 复制代码
如果对丢失的值和错误的值做出反应是很重要的,你可以捕获MissingClaimException或IncorrectClaimException,而不是捕获InvalidClaimException:
- try {
- Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
- } catch(MissingClaimException mce) {
- // the parsed JWT did not have the sub field
- } catch(IncorrectClaimException ice) {
- // the parsed JWT had a sub field, but its value was not equal to 'jsmith'
- }
- 复制代码
- Jwts.builder()
- .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP
- // .. etc ...
- 复制代码
如果您使用DEFLATE或GZIP压缩编解码器-就是这样,您就完成了。在解析过程中,您不必做任何事情,也不必配置JwtParser来进行压缩——JJWT将按照预期自动解压正文。