JJWT Github Repository
JJWT
旨在成为最容易使用和理解的库,用于在JVM
和Android
上创建和验证JSON Web令牌(JWT)
。
JJWT
是一个纯Java实现,完全基于JWT
,JWS
,JWE
,JWK
和JWA RFC
规范以及Apache 2.0
许可条款下的开源。
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.5version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-implartifactId>
<version>0.10.5version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-jacksonartifactId>
<version>0.10.5version>
<scope>runtimescope>
dependency>
上面的依赖声明只有一个编译时依赖项,其余的声明为运行时依赖项。这是因为JJWT在应用程序中明确设计的API,使其所有其他内部实现细节降级为仅运行时依赖项。
永远不要使用编译范围将jjwt-impl .jar
添加到项目中,始终使用运行时范围声明它。精心策划jjwt-api .jar
并确保它包含的内容并尽可能保持向后兼容。
运行时jjwt-impl .jar
策略为JJWT
开发人员提供了随时随地更改内部包和实现的灵活性。
@Test
public void getJWTTest() {
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String jws = getJwtStr(key);
if (log.isDebugEnabled()) {
log.debug(jws);
}
}
private String getJwtStr(Key key) {
return Jwts.builder()
.setSubject("JDKONG")
.signWith(key)
.compact();
}
是不是很简单!
在以上代码中,构建的过程如下:
JDKONG
的JWT
;HMAC-SHA-256
算法的密钥对JWT
进行签名;JWT
称为JWS
。最终生成的JWT
如下所示:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKREtPTkcifQ.C-eSTnoK-lryYVerB6SCbgbTRMKpXyWvDJNNPH07g3Q
现在,通过类似的方式验证JWT:
@Test
public void parseJwtStr() {
// 得到密钥
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 得到 JWT
String jwtStr = getJwtStr(key);
// 验证 JWT
assert Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody()
.getSubject()
.equals("JDKONG");
}
这里需要注意两件事:
之前的密钥用于验证JWT
的签名。 如果它无法验证JWT
,则抛出SignatureException
(从JwtException扩展)。
如果JWT
已经过验,会接着断言该claim
设置为JDKONG
。如果都没问题,则验证通过。
如果,在验证的过程中失败了会怎样呢?其实,在做JWT
解析时,我们可以捕捉异常JwtException
,比如:
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws);
//OK, we can trust this JWT
} catch (JwtException e) {
//don't trust the JWT!
}
JWT
本身是支持加密签名的,在使用签名的JWT
时,需要注意一下两点:
真实性
和完整性
保证JWT
包含可以信任的信息。 如果JWT
未通过真实性或完整性检查,应该始终拒绝JWT
,因为我们无法信任它。
那么JWT
如何签约? 让我们通过一些易于阅读的伪代码来完成它:
假设我们有一个带有JSON头和主体的JWT:
header
{
"alg": "HS256"
}
body
{
"sub": "JDKOGN"
}
删除JSON中所有不必要的空格:
String header = '{"alg":"HS256"}'
String claims = '{"sub":"JDKONG"}'
UTF_8
编码:String encodedHeader = base64URLEncode( header.getBytes("UTF-8") )
String encodedClaims = base64URLEncode( claims.getBytes("UTF-8") )
Header
和Body
使用.
进行分隔,并连接成一个字符串:String concatenated = encodedHeader + '.' + encodedClaims
Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )
Base64URL
对签名进行编码并使用.
将 它连接到字符串concatenated
后面:String jws = concatenated + '.' + base64URLEncode( signature )
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
这被称为
JWS
- 签名JWT
的简称。
下面讨论一下签名算法和密钥,特别是它们与JWT规范相关的内容。了解这些对于能够正确创建JWS至关重要。
JWT规范确定了12种标准签名算法–3种密钥算法和9种非对称密钥算法 - 由以下名称标识:
HS256
: HMAC using SHA-256HS384
: HMAC using SHA-384HS512
: HMAC using SHA-512ES256
: ECDSA using P-256 and SHA-256ES384
: ECDSA using P-384 and SHA-384ES512
: ECDSA using P-521 and SHA-512RS256
: RSASSA-PKCS-v1_5 using SHA-256RS384
: RSASSA-PKCS-v1_5 using SHA-384RS512
: RSASSA-PKCS-v1_5 using SHA-512PS256
: RSASSA-PSS using SHA-256 and MGF1 with SHA-256PS384
: RSASSA-PSS using SHA-384 and MGF1 with SHA-384PS512
: RSASSA-PSS using SHA-512 and MGF1 with SHA-512这些都在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
都要求每个RFC 7512
第3.3和3.5节的最小密钥长度(也称为RSA模数位长度)为2048位。 任何小于此值的内容(例如1024位)都将被拒绝,并抛出异常InvalidKeyException
。
也就是说,为了与最佳实践保持一致并增加键长度,JJWT
建议考虑使用的:
RS256
和PS256
至少有2048位密钥RS384
和PS384
至少3072位密钥RS512
和PS512
至少4096位密钥这些只是JJWT的建议而非要求。 JJWT仅强制执行JWT规范要求,对于任何RSA密钥,要求是RSA密钥(模数)长度,必须> = 2048位。
Elliptic Curve
JWT
椭圆曲线签名算法ES256
,ES384
和ES512
都需要最小密钥长度(也称为椭圆曲线顺序位长度),其至少与RFC 7512第3.4节中算法签名的各个R和S分量一样多。 这意味着:
ES256
要求您使用至少256位(32字节)长的私钥。ES384
要求您使用长度至少为384位(48字节)的私钥。ES512
要求您使用长度至少为512位(64字节)的私钥。首先,可以按如下方式创建JWS
:
Jwts.builder()
方法创建JwtBuilder
实例。JwtBuilder
方法根据需要添加标头参数和声明。JWT
进行签名的SecretKey
或非对称PrivateKey
。compact()
方法进行压缩和签名,生成最终的jws
。例如:
String jws = Jwts.builder() // (1)
.setSubject("JDKONG") // (2)
.signWith(key) // (3)
.compact(); // (4)
JWT Header
提供关于JWT Claims
相关的内容,格式和加密操作的元数据。
如果需要设置一个或多个JWT
头参数,则可以根据需要简单地多次调用JwtBuilder#setHeaderParam
,如下所示:
String jws = Jwts.builder()
.setHeaderParam("kid", "myKeyId")
// ... etc ...
每次调用setHeaderParam
时,它只是将键值对附加到内部Header
实例,如果键值已经存在,则会覆盖任何现有的同名键/值对。
注意:不需要设置
alg
或zip
标头参数,因为JJWT
会根据使用的签名算法或压缩算法自动设置它们。
除此之外,你还可以使用另外两种方式,设置JWT Header
,如下所示:
Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
.setHeader(header)
// ... etc ...
方式2 与 方式3 需要注意的是:调用setHeader
将覆盖任何现有的同名的key/value
对。 在所有情况下,JJWT
仍将设置(并覆盖)任何alg
和zip
标头,无论它们是否在指定的标头对象中。
Claims
是JWT
的正文部分,包含JWT
创建者希望向JWT
收件人提供的信息。
setIssuer
: sets the iss
(Issuer) ClaimsetSubject
: sets the sub
(Subject) ClaimsetAudience
: sets the aud
(Audience) ClaimsetExpiration
: sets the exp
(Expiration Time) ClaimsetNotBefore
: sets the nbf
(Not Before) ClaimsetIssuedAt
: sets the iat
(Issued At) ClaimsetId
: sets the jti
(JWT ID) Claim例如:
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 ...
自定义 Claims
如果需要设置一个或多个与上面显示的标准setter方法声明不匹配的自定义声明,可以根据需要多次调用JwtBuilder#claim
声明:
String jws = Jwts.builder()
.claim("hello", "world")
// ... etc ...
每次调用claim
时,它只是将键值对附加到内部claims
实例,如果键值已经存在,则会覆盖任何现有的同名key/value
对。
同上,你还可以使用另外两种方式,设置JWT Claims
,如下所示:
Claims claims = Jwts.claims();
populate(claims); //implement me
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...
Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
.setClaims(claims)
// ... etc ...
同样,方式2 与 方式3 需要注意的是:调用setClaims
将覆盖任何现有的同名的key/value
对。
建议通过调用JwtBuilder
的signWith
方法来指定签名密钥,并让JJWT
确定指定密钥允许的最安全算法:
String jws = Jwts.builder()
// ... etc ...
.signWith(key) // <---
.compact();
例如,如果使用长度为256位(32字节)的SecretKey
调用signWith
,则对于HS384
或HS512
,它不够强大,因此JJWT
将使用HS256
自动对JWT
进行签名。
使用signWith
时,JJWT
还会自动使用相关的算法标识符设置所需的alg
头。
类似地,如果使用长度为4096位的RSA PrivateKey
调用signWith
,JJWT
将使用RS512
算法并自动将alg
头设置为RS512
。
相同的选择逻辑适用于Elliptic Curve PrivateKeys
。
注意:你不能用
PublicKeys
签署JWT
,因为这总是不安全的。JJWT
将拒绝任何指定的PublicKey
的方式签名,并抛出异常:InvalidKeyException
。
在某些特定情况下,您可能希望覆盖给定键的JJWT
默认选定算法。
例如,如果有一个2048位
的RSA PrivateKey
,JJWT
会自动选择RS256
算法。 如果使用RS384
或RS512
,可以使用重载的signWith
方法手动指定它,该方法接受SignatureAlgorithm
作为附加参数:
.signWith(privateKey, SignatureAlgorithm.RS512) // <---
.compact();
这是允许的,因为JWT规范允许任何RSA密钥> = 2048位
的任何RSA
算法强度。JJWT
只需要RS512的键> = 4096位
,然后是RS384
,键> = 3072位
,最后是RS256
,键> = 2048位
。
但是,无论您选择哪种算法,JJWT
都会断言,根据JWT
规范要求,允许将指定的密钥用于该算法。
按如下方式解析JWS
:
Jwts.parser()
方法创建JwtParser
实例。JWS
签名的SecretKey
或非对称PublicKey
jws String
调用parseClaimsJws(String)
方法,生成原始JWS
。try/catch
块中,以防解析或签名验证失败。Jws<Claims> jws;
try {
jws = Jwts.parser() // (1)
.setSigningKey(key) // (2)
.parseClaimsJws(jwsString); // (3)
// we can safely trust the JWT
catch (JwtException ex) { // (4)
// we cannot use the JWT as intended by its creator
}
阅读JWS
时,最重要的事情是指定用于验证JWS加密签名的密钥。 如果签名验证失败,则无法安全地信任此JWT
,应将其丢弃。
那么我们使用哪个密钥进行验证?
如果jws
是使用SecretKey
签名的,则应在JwtParser
上指定相同的SecretKey
。 例如:
Jwts.parser()
.setSigningKey(secretKey) // <----
.parseClaimsJws(jwsString);
如果jws
是使用PrivateKey
签名的,那么应该在JwtParser
上指定该密钥相应的PublicKey
(不是PrivateKey
)。 例如:
Jwts.parser()
.setSigningKey(publicKey) // <---- publicKey, not privateKey
.parseClaimsJws(jwsString);
如果你的应用程序不只使用一个SecretKey或KeyPair会怎么样? 如果可以使用不同的SecretKeys或公钥/私钥或两者的组合创建JWS,该怎么办?
在这些情况下,无法使用单个键调用JwtParser
的setSigningKey
方法。相反,需要使用SigningKeyResolver
,接下来介绍。
如果程序需要使用不同密钥签名的JWS
,则不会调用setSigningKey
方法。 相反,需要实现SigningKeyResolver
接口并通过setSigningKeyResolver
方法在JwtParser
上指定实例。例如:
SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();
Jwts.parser()
.setSigningKeyResolver(signingKeyResolver) // <----
.parseClaimsJws(jwsString);
事实上,可以通过从SigningKeyResolverAdapter
扩展并实现resolveSigningKey(JwsHeader,Claims)
方法来简化一些事情。 例如:
public class MySigningKeyResolver extends SigningKeyResolverAdapter {
@Override
public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) {
// implement me
}
}
在解析JWS JSON
之后,JwtParser
将在验证jws
签名之前调用resolveSigningKey()
方法。 这也就允许检查Jws Header
和Claims
参数,以帮助查找用于验证特定jws
的密钥的信息。 这对于复杂安全模型的应用程序非常强大,这些安全模型可能在不同时间使用不同的密钥或针对不同的用户或客。
JWT
规范支持的方法是在创建JWS
时在JWS
头中设置kid(Key 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) {
//inspect the header or claims, lookup and return the signing key
String keyId = jwsHeader.getKeyId(); //or any other field that you need to inspect
Key key = lookupVerificationKey(keyId); //implement me
return key;
}
}
注意,检查
jwsHeader.getKeyId()
只是查找密钥的最常用方法,也可以检查任意数量的标头字段或声明,以确定如何查找验证密钥。
最后要记住,对于
HMAC
算法,返回的验证密钥应该是SecretKey
,对于非对称算法,返回的密钥应该是PublicKey
(不是PrivateKey
)。
假设你要求正在解析的JWS
具有特定的子sub
值,否则可能不信任该令牌。 那么可以使用JwtParser
上的各种require *
方法之一来实现:
try {
Jwts.parser().requireSubject("JDKONG").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {
// the sub field was missing or did not have a 'JDKONG' value
}
如果缺少某个值而不是不正确的值,那么就不会捕获InvalidClaimException
,而是捕获MissingClaimException
或IncorrectClaimException
:
try {
Jwts.parser().requireSubject("JDKONG").setSigningKey(key).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 'JDKONG'
}
当然,也可以使用require(fieldName,requiredFieldValue)
方法来要求自定义字段。例如:
try {
Jwts.parser().require("field","requiredValue").setSigningKey(key).parseClaimsJws(s);
} catch(InvalidClaimException ice) {
// the 'myfield' field was missing or did not have a 'myRequiredValue' value
}
请参阅JwtParser类
或JavaDoc
以获取可用于声明断言的各种require *
方法的完整列表。
JWT
规范仅为标准化JWE
(加密JWT)而非JWS
(签名JWT),但JJWT
支持两者。 如果您肯定使用JJWT创建的JWS也将使用JJWT进行解析,则可以将此功能与JWS一起使用,否则最好只将其用于JWE。
如果JWT
的Claim
集足够大,也就是说,它包含许多key/value
对,或者单个值非常大或冗长,那么可以通过压缩声明主体来减小创建的JWS
的大小。
例如,如果在URL
中使用生成的JWS
,压缩可能会很重要,因为由于浏览器,用户邮件代理或HTTP网关兼容性问题,URL
最好保持在4096
个字符以下。 较小的JWT
还有助于降低带宽利用率。
如果要压缩JWT
,可以使用JwtBuilde
r的compressWith(CompressionAlgorithm)
方法。 例如:
Jwts.builder()
.compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP
// .. etc ...
使用DEFLATE
或GZIP
压缩编解码器,但是在解压缩时,不必执行任何操作,不需要配置JwtParser
,JJWT
将按预期自动解压缩主体。
如果在创建JWT
时使用自己的自定义压缩编解码器(通过JwtBuilder compressWith
),则需要使用setCompressionCodecResolver
方法将编解码器提供给JwtParser
。 例如:
CompressionCodecResolver ccr = new MyCompressionCodecResolver();
Jwts.parser()
.setCompressionCodecResolver(ccr) // <----
// .. etc ...
通常,CompressionCodecResolver
实现将检查zip
标头以找出使用的算法,然后返回支持该算法的编解码器实例。 例如:
public class MyCompressionCodecResolver implements CompressionCodecResolver {
@Override
public CompressionCodec resolveCompressionCodec(Header header) throws CompressionException {
String alg = header.getCompressionAlgorithm();
CompressionCodec codec = getCompressionCodec(alg); //implement me
return codec;
}
}