Java Web Token 之 JJWT 使用

文章目录

    • Java Web Token 之 JJWT 使用
      • 1. JJWT 简介
      • 2. JJWT 引入
        • 2.1 Maven依赖
      • 3. 快速入门
        • 3.1 构建 JWT
        • 3.2 解析 JWT
      • 4. JWT 加密签名实现
        • 4.1 JWT 签名算法介绍
          • 4.1.1 支持算法类型
          • 4.1.2 算法使用要求
        • 4.2 创建 JWS
          • 4.2.1 设置 Header Parameters
          • 4.2.2 设置 Claims
          • 标准的 Claims
          • 4.2.3 签名 Signing Key
        • 4.3 解析 JWS
          • 4.3.1 校验 Key
          • 4.3.2 签名密钥解析器 Signing Key Resolver
          • 4.3.3 Claims 断言
      • 5. 压缩
        • 5.1 默认压缩
        • 5.2 自定义压缩
      • Learn More
      • 参考文档

Java Web Token 之 JJWT 使用

1. JJWT 简介

JJWT Github Repository

JJWT旨在成为最容易使用和理解的库,用于在JVMAndroid上创建和验证JSON Web令牌(JWT)

JJWT是一个纯Java实现,完全基于JWTJWSJWEJWKJWA RFC规范以及Apache 2.0许可条款下的开源。

2. JJWT 引入

2.1 Maven依赖

<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开发人员提供了随时随地更改内部包和实现的灵活性。

3. 快速入门

3.1 构建 JWT

@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();
}

是不是很简单!

在以上代码中,构建的过程如下:

  • 构建一个主题为JDKONGJWT
  • 使用适用于HMAC-SHA-256算法的密钥对JWT进行签名;
  • 最后,将它压缩成最终的String形式。 签名的JWT称为JWS

最终生成的JWT如下所示:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKREtPTkcifQ.C-eSTnoK-lryYVerB6SCbgbTRMKpXyWvDJNNPH07g3Q

3.2 解析 JWT

现在,通过类似的方式验证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!
}

4. JWT 加密签名实现

JWT本身是支持加密签名的,在使用签名的JWT时,需要注意一下两点:

  • 保证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") )
  • 将编码后的HeaderBody使用.进行分隔,并连接成一个字符串:
String concatenated = encodedHeader + '.' + encodedClaims
  • 使用加密秘密或私钥,选择的签名算法(此处使用HMAC-SHA-256),并对连接的字符串进行签名:
Key key = getMySecretKey()
byte[] signature = hmacSha256( concatenated, key )
  • 由于签名始终结果是字节数组,因此Base64URL对签名进行编码并使用.将 它连接到字符串concatenated后面:
String jws = concatenated + '.' + base64URLEncode( signature )
  • 最后生成的结果:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4

这被称为JWS - 签名JWT的简称。

下面讨论一下签名算法和密钥,特别是它们与JWT规范相关的内容。了解这些对于能够正确创建JWS至关重要。

4.1 JWT 签名算法介绍

4.1.1 支持算法类型

JWT规范确定了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枚举类中表示。

除了它们的安全属性之外,这些算法真正重要的是JWT规范 RFC 7518第3.2到3.5节 强制要求必须使用对所选算法足够强大的密钥

这意味着JJWT也会强制使用足够强的密钥。 如果为给定算法提 供弱键,JJWT将拒绝它并抛出异常。

JWT规范以及JJWT规定密钥长度的原因在于,如果不遵守算法的强制密钥属性,特定算法的安全模型可能完全崩溃,实际上根本没有安全性,这将会导致完全不安全的JWT

4.1.2 算法使用要求
  • HMAC-SHA

    JWT HMAC-SHA签名算法HS256,HS384和HS512需要一个密钥,该密钥至少与 RFC 7512第3.2节 中算法的签名(摘要)长度一样多。 这意味着:

    1. HS256HMAC-SHA-256,它产生256位(32字节)长的摘要,因此HS256要求您使用至少32字节长的密钥。
    2. HS384HMAC-SHA-384,它产生384位(48字节)长的摘要,因此HS384要求您使用至少48字节长的密钥。
    3. HS512HMAC-SHA-512,它产生512位(64字节)长的摘要,因此HS512要求您使用至少64字节长的密钥。
  • RSA

    JWT RSA签名算法RS256RS384RS512PS256PS384PS512都要求每个RFC 7512第3.3和3.5节的最小密钥长度(也称为RSA模数位长度)为2048位。 任何小于此值的内容(例如1024位)都将被拒绝,并抛出异常InvalidKeyException

    也就是说,为了与最佳实践保持一致并增加键长度,JJWT建议考虑使用的:

    1. RS256PS256至少有2048位密钥
    2. RS384PS384至少3072位密钥
    3. RS512PS512至少4096位密钥

    这些只是JJWT的建议而非要求。 JJWT仅强制执行JWT规范要求,对于任何RSA密钥,要求是RSA密钥(模数)长度,必须> = 2048位。

  • Elliptic Curve

    JWT椭圆曲线签名算法ES256ES384ES512都需要最小密钥长度(也称为椭圆曲线顺序位长度),其至少与RFC 7512第3.4节中算法签名的各个R和S分量一样多。 这意味着:

    1. ES256要求您使用至少256位(32字节)长的私钥。
    2. ES384要求您使用长度至少为384位(48字节)的私钥。
    3. ES512要求您使用长度至少为512位(64字节)的私钥。

4.2 创建 JWS

首先,可以按如下方式创建JWS

  • 使用Jwts.builder()方法创建JwtBuilder实例。
  • 调用JwtBuilder方法根据需要添加标头参数和声明。
  • 指定要用于对JWT进行签名的SecretKey非对称PrivateKey
  • 最后,调用compact()方法进行压缩和签名,生成最终的jws

例如:

String jws = Jwts.builder() 	// (1)
    .setSubject("JDKONG")      	// (2) 
    .signWith(key)          	// (3)
    .compact();             	// (4)
4.2.1 设置 Header Parameters

JWT Header提供关于JWT Claims相关的内容,格式和加密操作的元数据。

如果需要设置一个或多个JWT头参数,则可以根据需要简单地多次调用JwtBuilder#setHeaderParam,如下所示:

String jws = Jwts.builder()
    .setHeaderParam("kid", "myKeyId")
    // ... etc ...

每次调用setHeaderParam时,它只是将键值对附加到内部Header实例,如果键值已经存在,则会覆盖任何现有的同名键/值对。

注意:不需要设置algzip标头参数,因为JJWT会根据使用的签名算法或压缩算法自动设置它们。

除此之外,你还可以使用另外两种方式,设置JWT Header,如下所示:

  • 方式 2:
Header header = Jwts.header();
populate(header); //implement me
String jws = Jwts.builder()
    .setHeader(header)
    // ... etc ...
  • 方式 3:
Map<String,Object> header = getMyHeaderMap(); //implement me
String jws = Jwts.builder()
    .setHeader(header)
    // ... etc ...

方式2 与 方式3 需要注意的是:调用setHeader将覆盖任何现有的同名的key/value对。 在所有情况下,JJWT仍将设置(并覆盖)任何algzip标头,无论它们是否在指定的标头对象中。

4.2.2 设置 Claims

ClaimsJWT的正文部分,包含JWT创建者希望向JWT收件人提供的信息。

  • 标准的 Claims
    • 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
        .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,如下所示:

  • 方式 2:
Claims claims = Jwts.claims();
populate(claims); 			//implement me
String jws = Jwts.builder()
    .setClaims(claims)
    // ... etc ...
  • 方式 3:
Map<String,Object> claims = getMyClaimsMap(); //implement me
String jws = Jwts.builder()
    .setClaims(claims)
    // ... etc ...

同样,方式2 与 方式3 需要注意的是:调用setClaims将覆盖任何现有的同名的key/value对。

4.2.3 签名 Signing Key

建议通过调用JwtBuildersignWith方法来指定签名密钥,并让JJWT确定指定密钥允许的最安全算法:

String jws = Jwts.builder()
   // ... etc ...
   .signWith(key) // <---
   .compact();

例如,如果使用长度为256位(32字节)的SecretKey调用signWith,则对于HS384HS512,它不够强大,因此JJWT将使用HS256自动对JWT进行签名。

使用signWith时,JJWT还会自动使用相关的算法标识符设置所需的alg头。

类似地,如果使用长度为4096位的RSA PrivateKey调用signWithJJWT将使用RS512算法并自动将alg头设置为RS512

相同的选择逻辑适用于Elliptic Curve PrivateKeys

注意:你不能用PublicKeys签署JWT,因为这总是不安全的。 JJWT将拒绝任何指定的PublicKey的方式签名,并抛出异常:InvalidKeyException

  • 自定义 签名算法

在某些特定情况下,您可能希望覆盖给定键的JJWT默认选定算法。

例如,如果有一个2048位RSA PrivateKeyJJWT会自动选择RS256算法。 如果使用RS384RS512,可以使用重载的signWith方法手动指定它,该方法接受SignatureAlgorithm作为附加参数:

   .signWith(privateKey, SignatureAlgorithm.RS512) // <---
   .compact();

这是允许的,因为JWT规范允许任何RSA密钥> = 2048位的任何RSA算法强度。JJWT只需要RS512的键> = 4096位,然后是RS384键> = 3072位,最后是RS256键> = 2048位

但是,无论您选择哪种算法,JJWT都会断言,根据JWT规范要求,允许将指定的密钥用于该算法。

4.3 解析 JWS

按如下方式解析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
}
4.3.1 校验 Key

阅读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,该怎么办?

在这些情况下,无法使用单个键调用JwtParsersetSigningKey方法。相反,需要使用SigningKeyResolver,接下来介绍。

4.3.2 签名密钥解析器 Signing Key Resolver

如果程序需要使用不同密钥签名的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 HeaderClaims参数,以帮助查找用于验证特定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)。

4.3.3 Claims 断言

假设你要求正在解析的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,而是捕获MissingClaimExceptionIncorrectClaimException

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 *方法的完整列表。

5. 压缩

JWT规范仅为标准化JWE(加密JWT)而非JWS(签名JWT),但JJWT支持两者。 如果您肯定使用JJWT创建的JWS也将使用JJWT进行解析,则可以将此功能与JWS一起使用,否则最好只将其用于JWE。

如果JWTClaim集足够大,也就是说,它包含许多key/value对,或者单个值非常大或冗长,那么可以通过压缩声明主体来减小创建的JWS的大小

例如,如果在URL中使用生成的JWS,压缩可能会很重要,因为由于浏览器,用户邮件代理或HTTP网关兼容性问题,URL最好保持在4096个字符以下。 较小的JWT还有助于降低带宽利用率。

5.1 默认压缩

如果要压缩JWT,可以使用JwtBuilder的compressWith(CompressionAlgorithm)方法。 例如:

   Jwts.builder()
   .compressWith(CompressionCodecs.DEFLATE) // or CompressionCodecs.GZIP
   // .. etc ...

使用DEFLATEGZIP压缩编解码器,但是在解压缩时,不必执行任何操作,不需要配置JwtParserJJWT将按预期自动解压缩主体。

5.2 自定义压缩

如果在创建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;
    }
}

Learn More

  • JSON Web Token for Java and Android
  • How to Create and Verify JWTs in Java
  • Where to Store Your JWTs - Cookies vs HTML5 Web Storage
  • Use JWT the Right Way!
  • Token Authentication for Java Applications
  • JJWT Changelog

参考文档

  • jjwt

你可能感兴趣的:(JWT)