开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
关于依赖
<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>
-->
快速开始
生成JWS
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();
- 调用Jwts.builder()创建JWT实例;
- 设置subject=joe
- 使用key签名
- 调用compact()生成jws
输出如下: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
验证
通常我们需要对拿到的JWS进行验证,以丢弃错误的JWS。
assert Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws)
.getBody().getSubject().equals("Joe");
- 首先验证jws的签名是否正确
- 验证subject是否等于joe
异常
如果解析错误,可通过接收异常进行判断:
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(compactJws);
//OK, we can trust this JWT
} catch (JwtException e) {
//don't trust the JWT!
}
JWTs签名
签名首先保证jwts是正确的,然后保证没有被篡改
签名过程(伪代码)
- 假如有以下header、body内容:
// header
{
"alg": "HS256"
}
//body
{
"sub": "Joe"
}
- 删除不必要的空白:
String header = '{"alg":"HS256"}'
String claims = '{"sub":"Joe"}'
- 获取UTF-8字节和base64url编码
String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
- 将编码后的头文件和声明文件之间用句点字符连接起来
String concatenated = encodedHeader + '.' + encodedClaims
- 使用足够强的加密密钥或私钥,以及您选择的签名算法(这里我们将使用HMAC-SHA-256),并对连接的字符串签名:
Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )
- 因为签名总是字节数组,所以base64url对签名进行编码并附加句号字符'。',并将其转换为连接的字符串:
String jws = concatenated + '.' + base64URLEncode( signature )
- 最终得到的结果:
eyJhbGciOiJIUzI1NiJ9``.``eyJzdWIiOiJKb2UifQ``.``1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
实际上没有这么复杂,这里只是说明一个过程,JJWT提供了更简单的API来操作这一切。
签名密钥
JJWT提供了12种签名密钥,其中3种对称加密和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
可以通过枚举来使用: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
创建JWS
String jws = Jwts.builder() // 使用Jwts.builder()方法创建一个JwtBuilder实例
.setSubject("Bob") // 调用JwtBuilder方法,根据需要添加头参数和声明
.signWith(key) // 指定想要用来为JWT签名的SecretKey或非对称PrivateKey
.compact(); // 最后,调用compact()方法进行压缩和签名,生成最终的jws
header
JWT Header提供了与JWT的Claims相关的内容、格式和加密操作的元数据。
- 如果你需要设置一个或多个JWT头参数,比如kid (Key ID)头参数,你可以根据需要简单地调用JwtBuilder setHeaderParam一次或多次:
String jws = Jwts.builder()
.setHeaderParam("kid", "myKeyId")
// ... etc ...
每次setHeaderParam被调用时,它只是简单地将键值对附加到一个内部的Header实例,可能会覆盖任何现有的同名键值对。
注意:您不需要设置alg或zip报头参数,因为JJWT会根据使用的签名算法或压缩算法自动设置它们。
- 如果你想一次性指定整个头文件,你可以使用Jwts.header()方法并使用它来构建头文件的参数:
Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder().setHeader(header)
// ... etc ...
注意:调用setHeader将覆盖任何现有的头的名称/值对,这些名称可能已经被设置。然而,在所有情况下,JJWT仍然会设置(并覆盖)任何alg和zip头文件,无论它们是否在指定的头文件对象中。
- 如果你想一次性指定整个头文件,而又不想使用Jwts.header(),你可以使用JwtBuilder的setHeader(Map)方法:
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder().setHeader(header)
// ... etc ...
注意:调用setHeader将覆盖任何现有的头的名称/值对,这些名称可能已经被设置。然而,在所有情况下,JJWT仍然会设置(并覆盖)任何alg和zip头文件,无论它们是否在指定的头文件对象中。
Claims
claims是JWT的“主体”,包含JWT创建者希望呈现给JWT接收者的信息。
标准的Claims
JwtBuilder为JWT规范中定义的标准注册Claim名称提供了方便的setter方法。它们是:
- setIssuer: sets the iss (Issuer) Claim
- setSubject: sets the sub (Subject) Claim
- setAudience: sets the aud (Audience) Claim
- setExpiration: sets the exp (Expiration Time) Claim
- setNotBefore: sets the nbf (Not Before) Claim
- setIssuedAt: sets the iat (Issued At) Claim
- setId: sets the jti (JWT ID) Claim
例如:
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 ...
自定义Claims
如果你需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,你可以根据需要简单地调用JwtBuilder声明一次或多次:
String jws = Jwts.builder()
.claim("hello", "world")
// ... etc ...
每次调用claim时,它只是将键-值对附加到内部的Claims实例,可能会覆盖任何现有的同名键/值对。
显然,您不需要为任何标准的claim名称调用claim,建议相反调用标准的setter方法,这样可以增强可读性。
Claims实例
- 如果你想一次指定所有的索赔,你可以使用Jwts.claims()方法,并用它来构建索赔:
Claims claims = Jwts.claims();
populate(claims); //implement me
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...
注意:调用setClaims将用可能已经设置的相同名称覆盖任何现有的索赔名称/值对。
- 如果你想一次指定所有的声明,你又不想使用Jwts.claims(),你可以使用JwtBuilder的setClaims(Map)方法:
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方法参数。
- 如果你有一个encoded byte数组:SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes);
- 如果你有一个Base64-encoded字符串:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
- 如果你有一个Base64URL-encoded字符串:SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
- 如果你有一个原始的字符串(或者密码串之类):不建议使用!``SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
签名算法覆盖
在某些特定的情况下,您可能希望为给定的键覆盖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:
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。
那么我们用哪个键来验证呢?
- 如果jws是用一个SecretKey签名的,那么在JwtParserBuilder上应该指定相同的SecretKey。例如:
Jwts.parserBuilder()
.setSigningKey(secretKey) // <----
.build()
.parseClaimsJws(jwsString);
- 如果jws是用PrivateKey签名的,那么该key对应的PublicKey(不是PrivateKey)应该在JwtParserBuilder中指定。例如:
Jwts.parserBuilder()
.setSigningKey(publicKey) // <---- publicKey, not privateKey
.build()
.parseClaimsJws(jwsString);
但是您可能已经注意到—如果您的应用程序不只使用一个SecretKey或KeyPair怎么办?如果jws可以使用不同的秘钥或公钥/私钥,或两者的组合来创建呢?如果不能首先检查JWT,如何知道指定哪个键?
在这些情况下,你不能用一个键调用JwtParserBuilder的setSigningKey方法-相反,你将需要使用一个SigningKeyResolver,下面将介绍。
Signing Key Resolver
- 如果您的应用程序希望jws可以用不同的密钥签名,那么您不会调用setSigningKey方法。相反,你需要实现SigningKeyResolver接口,并通过setSigningKeyResolver方法在JwtParserBuilder上指定一个实例。
SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parserBuilder()
.setSigningKeyResolver(signingKeyResolver) // <----
.build()
.parseClaimsJws(jwsString);
- 您可以通过扩展SigningKeyResolverAdapter并实现resolveSigningKey(jwheader, Claims)方法来稍微简化一下。例如:
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)。
Claims断言
可以强制要求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'
}
压缩Compression
Jwts.builder()
.compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP
// .. etc ...
如果您使用DEFLATE或GZIP压缩编解码器-就是这样,您就完成了。在解析过程中,您不必做任何事情,也不必配置JwtParser来进行压缩——JJWT将按照预期自动解压正文。