JJWT使用详解
开始本文以前最好对JWT已经有一定的了解,这样会更方便您的阅读,本文仅仅对开源项目的使用文档进行汉化,更方便进行阅读和使用。下面附上链接:
简介
JJWT旨在成为最易于使用和理解的库,用于在JVM和Android上创建和验证JSON Web令牌(JWT)
Maven依赖
<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>
-->
快速开始
将依赖导入到IDEA,下面是JJWT一个最简单是实例
public class TestUse {
public static void main(String[] args) {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = Jwts.builder().setSubject("NB").signWith(key).compact();
System.out.println(jws);
}
}
- 根据指定的签名算法,安全随机生成一个SecretKey
- 建立一个JWT,它将 sub(主题)设置为NB
- 使用适用于HMAC-SHA-256算法的密钥对JWT进行签名。
- 最后compact将其压缩成最终
String
形式。签名的JWT称为“ JWS”。
打印成功输出以下内容:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJOQiJ9.0ksqf8htUXzKkE2lRIzgI3zte7kFBQ15LVv39ABiWRc
现在,让我们使用刚才生成的SecretKey验证一下JWT
System.out.println(
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(jws).getBody().getSubject().equals("NB")
); //true
签名的JWT
JWT规范提供了对JWT进行 密码签名的功能。签署JWT:
- 保证JWT是由我们认识的人(它是真实的)以及
- 保证在创建JWT后没有人操纵或更改过JWT(维护了JWT的完整性)。
这两个属性(真实性和完整性)确保我们JWT包含我们可以信任的信息。如果JWT无法通过真实性或完整性检查,我们应该始终拒绝该JWT,因为我们无法信任它。
那么,JWT如何签名?让我们通过一些易于阅读的伪代码来完成它:
-
假设我们有一个带有JSON标头和正文(又称“声明”)的JWT,如下所示:
标头
{ "alg": "HS256" }
身体
{ "sub": "Joe" }
-
删除JSON中所有不必要的空格:
String header = '{"alg":"HS256"}' String claims = '{"sub":"NB"}'
-
分别获取UTF-8字节和Base64URL编码:
String encodedHeader = base64URLEncode( header.getBytes("UTF-8") ) String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
-
将编码的标头和声明之间用句点字符连接起来:
String concatenated = encodedHeader + '.' + encodedClaims // eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJOQiJ9
-
使用足够强大的加密秘密或私钥,以及您选择的签名算法(我们将在此处使用HMAC-SHA-256),并对连接的字符串进行签名:
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); byte[] signature = hmacSha256( concatenated, key )
-
由于签名始终是字节数组,因此Base64URL对签名进行编码,并附加句点字符“.”。并将其连接到字符串:
String jws = concatenated + '.' + base64URLEncode( signature )
在这里,最终的jws
String如下所示:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJOQiJ9.0ksqf8htUXzKkE2lRIzgI3zte7kFBQ15LVv39ABiWRc
这称为“ JWS”-签名的JWT的缩写。
当然,没有人愿意在代码中手动执行此操作,更糟糕的是,如果您遇到任何错误,则可能导致安全问题或弱点。结果,创建了JJWT来为您处理所有这一切:JJWT完全为您自动创建JWS以及解析和验证JWS。
但是在深入向您展示如何使用JJWT创建JWS之前,让我们简要讨论签名算法和密钥,特别是与JWT规范相关的签名算法和密钥。了解它们对于能够正确创建JWS至关重要。
签名算法和密钥
JWT规范标识了12种标准签名算法-3种密钥算法和9种非对称密钥算法-由以下名称标识:
HS256
:使用SHA-256的HMACHS384
:使用SHA-384的HMACHS512
:使用SHA-512的HMACES256
:使用P-256和SHA-256的ECDSAES384
:使用P-384和SHA-384的ECDSAES512
:使用P-521和SHA-512的ECDSARS256
:使用SHA-256的RSASSA-PKCS-v1_5RS384
:使用SHA-384的RSASSA-PKCS-v1_5RS512
:使用SHA-512的RSASSA-PKCS-v1_5PS256
:使用SHA-256和MGF1与SHA-256的RSASSA-PSSPS384
:使用SHA-384和MGF1与SHA-384的RSASSA-PSSPS512
:使用SHA-512和MGF1与SHA-512的RSASSA-PSS
这些都在io.jsonwebtoken.SignatureAlgorithm
枚举中表示。
除了安全特性外,这些算法真正重要的是,JWT规范 RFC 7518第3.2至3.5节 要求(授权)您必须使用对于所选算法足够强的密钥。
这意味着JJWT(符合规范的库)还将强制您为选择的算法使用足够强的密钥。如果为给定算法提供弱密钥,JJWT将拒绝它并抛出异常。
我们保证,这不是因为我们要让您的生活变得艰难!JWT规范以及因此的JJWT规定密钥长度的原因是,如果不遵守算法的强制密钥属性,则特定算法的安全模型可能会完全崩溃,从而根本没有安全性。没有人想要完全不安全的JWT,对吗?我们也不会。
那么有什么要求?
HMAC SHA
JWT HMAC-SHA签名算法HS256
,HS384
并且HS512
需要一个秘密密钥,该密钥至少应与RFC 7512 3.2节中算法的签名(摘要)长度一样多。这表示:
HS256
是HMAC-SHA-256,它会生成256位(32字节)长的摘要,因此HS256
要求您使用至少32字节长的密钥。HS384
是HMAC-SHA-384,它会生成384位(48字节)长的摘要,因此HS384
要求您使用至少48字节长的密钥。HS512
是HMAC-SHA-512,它会生成512位(64字节)长的摘要,因此HS512
要求您使用至少64字节长的密钥。
RSA
JWT RSA签名算法RS256
,RS384
,RS512
,PS256
,PS384
和PS512
所有需要的最小的密钥长度(又名的RSA模数位长)2048
根据RFC 7512第比特 3.3和3.5。小于此值的任何内容(例如1024位)都将被拒绝InvalidKeyException
。
也就是说,为了确保最佳实践并增加密钥长度以延长安全性,JJWT建议您使用:
- 至少2048位按键,
RS256
并带有和PS256
- 至少有3072位按键,
RS384
并带有和PS384
- 至少4096位按键,
RS512
并带有和PS512
这些只是JJWT的建议,而不是要求。JJWT仅强制执行JWT规范要求,并且对于任何RSA密钥,该要求是RSA密钥(模数)长度(以位为单位)必须> = 2048位。
椭圆曲线
JWT椭圆曲线签名算法ES256
,ES384
和ES512
所有需要的最小的密钥长度(又名椭圆曲线阶位长度)是至少一样多的位的算法签名的个体 R
和S
每个部件RFC 7512第3.4节。这表示:
ES256
要求您使用至少256位(32字节)长的私钥。ES384
要求您使用至少384位(48字节)长的私钥。ES512
要求您使用至少512位(64字节)长的私钥。
创建安全密钥
如果您不想考虑位长的要求,或者只是想让生活更轻松,JJWT提供了io.jsonwebtoken.security.Keys
实用程序类,它可以为您可能要使用的任何给定JWT签名算法生成足够安全的密钥。
秘密钥匙
如果要生成足以SecretKey
与JWT HMAC-SHA算法一起使用的强度,请使用 Keys.secretKeyFor(SignatureAlgorithm)
辅助方法:
SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);//或HS384或HS512
在幕后,JJWT使用JCA提供程序KeyGenerator
创建给定算法的正确最小长度的安全随机密钥。
如果需要保存此新代码SecretKey
,则可以对Base64(或Base64URL)进行编码:
String secretString = Encoders.BASE64.encode(key.getEncoded());
确保将结果保存在secretString
安全的地方,Base64编码不是加密,因此仍被视为敏感信息。在使用时可以先进行对应的Base64(或Base64URL)进行解码
非对称密钥
如果要生成足够强的椭圆曲线或RSA非对称密钥对以与JWT ECDSA或RSA算法一起使用,请使用Keys.keyPairFor(SignatureAlgorithm)
辅助方法:
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512
您可以使用私钥(keyPair.getPrivate()
)创建JWS,并使用公钥(keyPair.getPublic()
)解析/验证JWS。
创建JWS
您可以如下创建一个JWS:
- 使用该
Jwts.builder()
方法创建JwtBuilder
实例。 - 调用
JwtBuilder
方法以根据需要添加标头参数和声明。 - 指定要用于签名JWT的
SecretKey
或不对称PrivateKey
。 - 最后,调用该
compact()
方法进行压缩和签名,生成最终的jws。
例如:
String jws = Jwts.builder() // (1)
.setSubject("NB") // (2)
.signWith(key) // (3)
.compact(); // (4)
标头Header
JWT标头提供有关与JWT索赔相关的内容,格式和加密操作的元数据。
如果您需要设置一个或多个JWT标头参数,例如kid
(Key ID)标头参数,则可以JwtBuilder
setHeaderParam
根据需要简单地调用 一次或多次:
String jws = Jwts.builder()
.setHeaderParam("kid", "myKeyId")
// ... etc ...
每次setHeaderParam
调用时,它只会将键值对附加到内部Header
实例,从而有可能覆盖任何现有的同名键/值对。
注意:您不需要设置alg
或zip
标头参数,因为JJWT会根据使用的签名算法或压缩算法自动设置它们。
标头Instance
如果要一次指定整个标头,则可以使用该Jwts.header()
方法并用它构建标头参数:
Header header = Jwts.header(); //这里的header可以直接使用put()放键值对
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...
注意:调用setHeader
将使用可能已经设置的相同名称覆盖所有现有的标头名称/值对。但是,在所有情况下,JJWT仍将设置(和覆盖)任何alg
和zip
标头,无论它们是否在指定的header
对象中。
标头Map
如果您想一次指定整个标头而又不想使用Jwts.header()
,则可以使用JwtBuilder
setHeader(Map)
method代替:
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...
注意:调用setHeader
将使用可能已经设置的相同名称覆盖所有现有的标头名称/值对。但是,在所有情况下,JJWT仍将设置(和覆盖)任何alg
和zip
标头,无论它们是否在指定的header
对象中。
声明Claims
声明是JWT的“主体”,并包含JWT创建者希望提供给JWT接收者的信息。这个才是我们关注的重点
标准声明
在JwtBuilder
提供了在JWT规范中定义的标准注册权利要求名方便setter方法。他们是:
setIssuer
:设置iss
(发行方)索赔setSubject
:设置sub
(主题)声明setAudience
:设置aud
(受众群体)声明setExpiration
:设置exp
(到期时间)声明setNotBefore
:设置nbf
(不早于)声明setIssuedAt
:设置iat
(签发)声明setId
:设置jti
(JWT ID)声明
String jws = Jwts.builder()
.setIssuer("me")
.setSubject("Bob")
.setAudience("you")
.setExpiration(expiration) //a java.util.Date
.setNotBefore(notBefore) //a java.util.Date
.setIssuedAt(new Date()) // for example, now
.setId(UUID.randomUUID()) //just an example id
/// ... etc ...
自定义声明
如果您需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,则可以JwtBuilder
claim
根据需要简单地调用一次或多次:
String jws = Jwts.builder()
.claim("hello", "world")
// ... etc ...
每次claim
调用时,它只会将键值对附加到内部Claims
实例,从而有可能覆盖任何现有的同名键/值对。
显然,您不需要调用claim
任何标准的声明名称,而建议您调用相应的标准setter方法,因为这可以提高可读性。
声明Instance
如果要一次指定所有声明,则可以使用Jwts.claims()
方法并以此建立声明:
Claims claims = Jwts.claims(); //这里的claims可以直接使用put()放键值对
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...
注意:呼叫setClaims
将使用可能已经设置的相同名称覆盖所有现有的索赔名称/值对。
声明Map
如果您想一次指定所有声明而又不想使用Jwts.claims()
,则可以使用JwtBuilder
setClaims(Map)
method代替:
Map < String,Object > Claims = getMyClaimsMap(); //实现我
String jws = Jwts.builder()
.setClaims(claims)// ...等...
签名密钥
建议您通过调用JwtBuilder
的signWith
方法来指定签名密钥,并让JJWT确定指定密钥所允许的最安全算法。
String jws = Jwts.builder()
// ... etc ...
.signWith(key) // <---
.compact();
例如,如果您signWith
使用SecretKey
256位(32字节)长的进行调用,则它对于HS384
或不够强大 HS512
,因此JJWT将使用进行自动签名HS256
。
使用signWith
JJWT时,还将自动设置所需的alg
标头以及相关的算法标识符。
同样,如果signWith
使用PrivateKey
4096位长的RSA进行调用,JJWT将使用该RS512
算法并将alg
标头自动设置为RS512
。
相同的选择逻辑适用于椭圆曲线PrivateKey
。
**注意:您不能使用PublicKey
s签名JWT,因为这始终是不安全的。**JJWT将拒绝使用PublicKey
签名的任何指定内容 InvalidKeyException
。
如果您的密钥是:
-
一个编码的字节数组:
SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes);
-
Base64编码的字符串:
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
-
Base64URL编码的字符串:
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretString));
-
原始(未编码)字符串(例如,密码字符串):
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
调用总是错误的
secretString.getBytes()
(不提供字符集)。但是,例如,
correcthorsebatterystaple
应尽可能避免使用这样的原始密码字符串,因为它们不可避免地会导致密钥弱或易受攻击。安全随机密钥几乎总是更强。如果可以的话,最好创建一个新的安全随机密钥。
签名算法替代
在某些特定情况下,您可能想要覆盖给定密钥的JJWT默认选择的算法。
例如,如果您具有PrivateKey
2048位的RSA ,JJWT将自动选择RS256
算法。如果要使用RS384
或RS512
代替,可以使用signWith
接受的重载方法手动指定它SignatureAlgorithm
作为附加参数:
.signWith(privateKey, SignatureAlgorithm.RS512) // <---
.compact();
这是允许的,因为JWT规范允许任何大于2048位的RSA密钥具有任何RSA算法强度。JJWT只喜欢RS512
> = 4096位RS384
的键,然后是> = 3072位RS256
的键,最后是> = 2048位的键。
但是,在所有情况下,无论您选择哪种算法,JJWT都会根据JWT规范要求断言允许将指定的密钥用于该算法。
解析JWS
您阅读(解析)JWS的方法如下:
- 使用该
Jwts.parserBuilder()
方法创建JwtParserBuilder
实例。 - 指定要用于验证JWS签名的
SecretKey
或不对称PublicKey
。1个 - 在
build()
上调用方法JwtParserBuilder
以返回线程安全JwtParser
。 - 最后,
parseClaimsJws(String)
用您的jws调用该方法String
,生成原始的JWS。 - 如果解析或签名验证失败,则整个调用将包装在try / catch块中。稍后我们将讨论异常和失败原因。
例如:
Jws<Claims> jws;
try {
jws = Jwts.parserBuilder() // (1)
.setSigningKey(key) // (2)
.build() // (3)
.parseClaimsJws(jwsString); // (4)
// 我们可以放心地信任JWT
catch (JwtException ex) { // (5)
// 我们不能按照其创建者的意图使用JWT
}
注意:如果您期望使用JWS,请始终调用JwtParser
的parseClaimsJws
方法(而不是其他可用的类似方法之一),因为这可以保证解析签名的JWT的正确安全模型。
验证码
读取JWS时最重要的事情是指定用于验证JWS的加密签名的密钥。如果签名验证失败,则无法安全地信任JWT,应将其丢弃。
那么,我们使用哪个密钥进行验证?
-
如果jws用
SecretKey
签名,则应指定相同SecretKey
。例如:Jwts.parserBuilder() .setSigningKey(secretKey) // <---- .build() .parseClaimsJws(jwsString);
-
如果jws用
PrivateKey
签名,则应在上指定PrivateKey
对应的密钥PublicKey
(不是PrivateKey
)。例如:Jwts.parserBuilder() .setSigningKey(publicKey) // <---- publicKey, not privateKey .build() .parseClaimsJws(jwsString);
但是您可能已经注意到了一些事情-如果您的应用程序不只使用单个SecretKey或KeyPair,该怎么办?如果可以使用不同的SecretKey
s或公用/专用密钥或两者结合来创建JWS ,该怎么办?如果您不能先检查JWT,您如何知道要指定哪个键?
在这些情况下,您无法通过单个键调用JwtParserBuilder
的setSigningKey
方法-而是需要使用SigningKeyResolver
,然后再覆盖。
签名密钥解析器
如果您的应用程序期望可以用不同的密钥签名的JWS,则不会调用该setSigningKey
方法。相反,您将需要实现 SigningKeyResolver
接口并JwtParserBuilder
通过setSigningKeyResolver
方法指定实例。
例如:
SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parserBuilder()
.setSigningKeyResolver(signingKeyResolver) // <----
.build()
.parseClaimsJws(jwsString);
您可以通过扩展SigningKeyResolverAdapter
和实现 resolveSigningKey(JwsHeader, Claims)
方法来稍微简化一些事情。例如:
public class MySigningKeyResolver extends SigningKeyResolverAdapter {
@Override
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
// implement me
}
}
在JwtParser
将调用resolveSigningKey
解析JWS JSON后的方法,但验证的JWS签名之前。这样,您就可以检查JwsHeader
和Claims
参数中是否有任何信息,可以帮助您查找Key
用于验证特定jws的信息。对于具有更复杂安全模型的应用程序来说,此功能非常强大,这些应用程序可能在不同时间使用不同的密钥,或者对于不同的用户或客户。
您可以检查哪些数据?
JWT规范支持的方法kid
是在创建JWS时在JWS标头中设置一个(密钥ID)字段,例如:
Key signingKey = getSigningKey();
String keyId = getKeyId(signingKey);
//any mechanism you have to associate a key with an ID is fine
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) {
检查头或权利要求中,查找并返回签名密钥字符串
String keyId = jwsHeader.getKeyId(); //或您需要检查的其他任何字段
Key key = lookupVerificationKey(keyId); //根据kid查找秘钥
return key;
}
}
请注意,检查jwsHeader.getKeyId()
只是查找密钥的最常用方法-您可以检查任意数量的标题字段或声明,以确定如何查找验证密钥。这完全基于JWS的创建方式。
最后要记住,对于HMAC算法,返回的验证密钥应为SecretKey
,对于非对称算法,返回的密钥应为PublicKey
(而不是PrivateKey
)。
声明断言
您可以强制要解析的JWS符合所需的期望,并且对您的应用程序很重要。
例如,假设您要求正在解析的JWS具有特定的sub
(主题)值,否则您可能不信任令牌。您可以通过使用以下各种require
*方法 之一来做到这一点JwtParserBuilder
:
try {
Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
} catch(InvalidClaimException ice) {
//sub字段丢失或没有'jsmith'值
}
如果对缺失值和不正确的值做出反应很重要InvalidClaimException
,则可以使用更加具体的异常MissingClaimException
和IncorrectClaimException
:
try { Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
} catch(MissingClaimException mce) {
// sub没有字段
} catch(IncorrectClaimException ice) {
// sub有字段但是,不是jsmith
}
您还可以通过使用require(fieldName, requiredFieldValue)
方法来要求自定义字段-例如:
try {
Jwts.parserBuilder().require("myfield", "myRequiredValue").setSigningKey(key).build().parseClaimsJws(s);
} catch(InvalidClaimException ice) {
// 'myfield' 字段丢失或者 没有myRequiredValue这个值
}
(或者再次,您可以捕捉MissingClaimException
或IncorrectClaimException
替代)
考虑时钟偏移
解析JWT时,您可能会发现exp
或nbf
声明断言失败(引发异常),因为解析机上的时钟与创建JWT的计算机上的时钟并不完全同步。这可能会导致由于明显的问题exp
,并nbf
是基于时间的断言,和时钟时间需要被可靠地同步对共享的断言。给解析者一点时间余地来解析
使用分析时,可以考虑这些差异(一般不超过几分钟)JwtParserBuilder
的 setAllowedClockSkewSeconds
。例如:
long seconds = 3 * 60; //3 minutes
Jwts.parserBuilder()
.setAllowedClockSkewSeconds(seconds) // <----
// ... etc ...
.build()
.parseClaimsJws(jwt);
这样可以确保忽略机器之间的时钟差异。两到三分钟应该足够了;如果生产机器的时钟与世界上大多数原子钟相差5分钟以上,那将是相当奇怪的。
自定义时钟支持
如果以上setAllowedClockSkewSeconds
内容不足以满足您的需求,则可以通过自定义时间源获取在进行时间戳比较的解析过程中创建的时间戳。调用JwtParserBuilder
的setClock
用的实现方法io.jsonwebtoken.Clock
的接口。例如:
Clock clock = new MyClock();
Jwts.parserBuilder().setClock(myClock) //... etc ...
该JwtParser
的默认Clock
实现只返回new Date()
时发生的解析来反映的时间,最期望的那样。但是,提供自己的时钟可能会很有用,尤其是在编写测试用例以确保确定性行为时。
Base64支持
JJWT使用非常快速的纯Java Base64编解码器进行Base64和Base64Url编码和解码,可以确保在所有JDK和Android环境中确定性地工作。
您可以使用io.jsonwebtoken.io.Encoders
和io.jsonwebtoken.io.Decoders
实用程序类访问JJWT的编码器和解码器。
io.jsonwebtoken.io.Encoders
:
io.jsonwebtoken.io.Decoders
: