JWT实战之升级Java JWT

概述

关于JWT的基础概念,如JWT组成部分,以及入门实战,如:如何生成Token、如何解析Token、怎么加入自定义字段等,可参考JWT入门教程

如前文提到的blog所述,大多数公司都会使用如下(版本)的Maven依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

看一下Maven仓库:
在这里插入图片描述
可以看到有64个CVE安全漏洞!很多!!

使用JWT(以及其他安全框架,如Spring Security或Spring Security OAuth2)的目标是加强应用的认证和鉴权,结果Java JWT工具包本身有这么多CVE安全隐患,有点搞笑了。

于是产生依赖库升级这样一个技术改造需求,本文记录升级遇到的问题。

问题

GAV变更

谈到依赖三方库升级,必然需要借助于Maven仓库。搜索不难发现,artifactId发生变更,需要引入如下依赖:

<properties>
    <jjwt.version>0.12.5</jjwt.version>
</properties>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>${jjwt.version}</version>
</dependency>

编译问题

在这里插入图片描述
如上截图,编译报错是比@deprecated API更严重的问题。使用过期的,即将废弃的API,即@deprecated API,IDEA会给出屎黄色的warning提示。一般而言,某个过期API,再经过几次版本升级迭代后,就会变成removed API,即编译报错,也就是上面截图看到的红色。执行mvn compile失败,应用启动失败。

代码片段如下:

public static Claims getClaimsFromToken(String token) {
    Claims claims;
    try {
        claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        claims = null;
    }
    return claims;
}

关于如何替换被标记为@deprecated或removed的API,一般都是看源码。

比如上面的setSigningKey源码注释里有提到in favor of verifyWith(SecretKey) as explained in the above Deprecation Notice and will be removed in 1.0.0.告知,故而可以考虑使用verifyWith(SecretKey)来作为替换。

但是如果版本升级跨度太大(API被废弃后标红,根本就不能通过IDEA去查看源码),或是开源代码维护者没有提供替换API等解决方案,则比较麻烦。此时一般都是去查看官方文档,或Google搜索。好在Java JWT提供维护良好的文档

调整后的代码片段如下:

public static Claims getClaimsFromToken(String token) {
    SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
    return Jwts.parser()
            .verifyWith(key)
            .build()
            .parseSignedClaims(token)
            .getPayload();
}

启动失败DefaultJwtBuilder

解决上面一个编译问题后,实际上还有另外一个废弃API的问题,后文再提。解决编译问题后,优先级自然是看应用能否启动,postman接口测试能否请求成功。结果遇到应用Debug模式启动失败的报错:

io.jsonwebtoken.lang.UnknownClassException: Unable to load class named [io.jsonwebtoken.impl.DefaultJwtBuilder] from the thread context, current, or system/application ClassLoaders.  All heuristics have been exhausted.  Class could not be found.  Have you remembered to include the jjwt-impl.jar in your runtime classpath?

报错提示已经很明显,通过查看maven私服仓库。稍加分析可得出结论:自0.10.0版本后,之前的一个依赖拆开为多个依赖:

加入以下依赖,重启应用,报错消失,应用可以正常启动。

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>${jjwt.version}</version>
</dependency>

解决得到问题。

WeakKeyException HS512 algorithm

应用启动成功,postman请求login接口,结果遇到如下报错信息:

io.jsonwebtoken.security.WeakKeyException: The signing key's size is 72 bits which is not secure enough for the HS512 algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HS512 MUST have a size >= 512 bits (the key size must be greater than or equal to the hash output size).  Consider using the io.jsonwebtoken.security.Keys class's 'secretKeyFor(SignatureAlgorithm.HS512)' method to create a key guaranteed to be secure enough for HS512.  See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.

大意就是使用的加密等级强度不够。之前遇到这类提示,一般都会选择性忽视;毕竟,能Run起来就说明能Work;warning嘛,有时间再去优化。结果好家伙,升级依赖后,直接报错,意思很明显:必须要提高密钥长度或使用破解难度更大的加密算法。

看了下代码,搜索HS512,发现是生成token的如下代码片段报错:

public static String generateToken(Map<String, Object> claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
}

上面还提到有个废弃API没有优化,正好就是此处被废弃的API:
在这里插入图片描述
那先解决废弃API的问题吧。搜索GitHub官方文档,优化调整后的代码片段如下:

public static String generateToken(Map<String, Object> claims) {
	SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));
	return Jwts.builder()
        .claims(claims)
        .expiration(generateExpirationDate())
        .signWith(key)
        .compact();
}

这里额外提一句:上面的报错提示是HS512算法。

一开始并没有找到HS512算法的示例代码,并没有做到升级前后【本质】保持不变的参考性原则。

WeakKeyException HMAC-SHA algorithm

没有找到HS512算法示例,使用Keys.hmacShaKeyFor()方法,postman接口调试报错信息如下:

io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.

根据报错提示,是HMAC-SHA算法,使用的密钥是private static final String SECRET = "ThisIsASecret";。长度不够?

于是无脑复制加长密钥,SECRET = "ThisIsASecretThisIsASecretThisIsASecretThisIsASecret",结果还真解决报错。

json Serializer

继续调试postman接口,又遇到报错信息如下:

io.jsonwebtoken.impl.lang.UnavailableImplementationException: Unable to find an implementation for interface io.jsonwebtoken.io.Serializer using java.util.ServiceLoader. Ensure you include a backing implementation .jar in the classpath, for example jjwt-jackson.jar, jjwt-gson.jar or jjwt-orgjson.jar, or your own .jar for custom implementations.

不难得知和上面的问题,加入以下依赖,重启应用解决问题:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>${jjwt.version}</version>
</dependency>

Compact JWT strings may not contain whitespace

现在登录接口login已经调试成功,postman里可以看到接口成功返回的JWT Token。后续的所有请求都需要带着这个Token。

继续调试其他接口,又遇到一个报错:Compact JWT strings may not contain whitespace.

有点懵啊。自认为我的英文阅读理解能力还挺不错啊,理智告诉我:JWT字符串不得包含空格

事实上,我生成的Token形式如下:eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiLDtcKHc1jCmcOCwrHDhcOMw5PDr8OPXHUwMDBCwobDim0iLCJleHAiOjE3MTAxNTM1OTd9.sKlGrAF7FFYk8hUZD7PeRddOG6azm_gNgnZ9a9mRu70

通过JWT在线解密网站,这个JWT可以成功解密:
在这里插入图片描述
说明生成的JWT Token是没有问题的。

回过头来,再仔细看看postman设置Token的选项,选择Bearer Token并没有问题啊:
在这里插入图片描述
空格??Bearer Token格式是Bearer jwt,经过排查,发现一个很傻的问题:

// 少了个空格
public static final String TOKEN_PREFIX = "Bearer ";

Bearer Token和JWT Bearer

与此同时,在排查上面的空格问题时,发现postman功能强大支持好几种格式的Token。
在这里插入图片描述
其中Bearer Token是我们最常使用的。JWT Bearer是什么?选择JWT Bearer后,发现如下下拉列表
在这里插入图片描述
等等,这里的算法列表,不正好有一个上面提到的HS512算法吗?

继续研究HS512的初始化SecretKey代码片段,还是在GitHub官网找到代码片段:

// SecretKey secret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));
SecretKey secret = Jwts.SIG.HS512.key().build();

使用下面这个HS512算法以及SecretKey这个API后,则无需再额外定义一个密钥字符串常量SECRET,当然也就不存在上面提到的密钥长度问题。

事实上,在写此文的过程中,认识到postman也在建议我们使用HS512加密算法,而不是HMAC-SHA算法。

使用HS512加密算法,生成的JWT Token更长一些:eyJhbGciOiJIUzUxMiJ9.eyJ1c2VySWQiOiLDtcKHc1jCmcOCwrHDhcOMw5PDr8OPXHUwMDBCwobDim0iLCJleHAiOjE3MTAxNzIyMTF9.ESVmAmm8rw9UGJG7he2EfTKz4xvYO5C5SSmkLbvEaK8VafKtOfPp64q8ONwDmQUoXsh0vn03ONFEeaQb9HqU_w

postman使用JWT Bearer,然后选择HS512算法,填入上面生成的Token,但是又遇到报错。

JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted

承接上一个章节,又遇到另一个报错,还真是无穷无尽啊:
JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.

TODO:未解决。还是使用HMAC-SHA算法,及postman使用Bearer Token吧。。

加密自定义字段

另外,再多扯几句。不难发现上面在线解密JWT的截图里,解密后payload里有个自定义字段userId

看到熟悉的乱码?别慌!!

由于JWT可以轻易被截取,并能被解密。但是在真实的业务场景开发中,我们又经常会遇到需要在JWT Token里加塞字段的需求,去渠道,商户Id等。如果业务交互需要某个敏感字段,如手机号,怎么办呢?加密一下:

HashMap<String, Object> claims = new HashMap<>();
// put any data in the map
map.put(USER_ID, EncryptUtil.encrypt(userId));

userId一般情况下都是无意义的UUID,上面的代码片段仅仅是demo示例。

附录

附录源码:

@Slf4j
public class JwtUtil {
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";
    public static final String USER_ID = "userId";
    private static final Long EXPIRATION_TIME = 3600000L; // 1 hour
    private static final String SECRET = "ThisIsASecretThisIsASecretThisIsASecretThisIsASecret";

    public static String generateToken(String userId) {
        HashMap<String, Object> map = new HashMap<>();
        // put any data in the map
        try {
            map.put(USER_ID, EncryptUtil.encrypt(userId));
        } catch (Exception e) {
            log.warn("Encryption failed.", e);
            throw new RuntimeException("Encryption failed");
        }
        return generateToken(map);
    }

    public static String generateToken(Map<String, Object> claims) {
        SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));
        return Jwts.builder()
                .claims(claims)
                .expiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(key)
                .compact();
    }

	public static Claims getClaimsFromToken(String token) {
	    SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
	    return Jwts.parser()
	            .verifyWith(key)
	            .build()
	            .parseSignedClaims(token)
	            .getPayload();
	}
}

参考

  • 21
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
JWT(JSON Web Token)是一种用于在网络上安全传输信息的开放标准(RFC 7519)。它使用JSON格式定义了一种紧凑且自包含的方式来传输数据,通常用于身份验证和授权场景。 一个JWT由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。头部通常包含了关于令牌的元数据和算法信息,载荷包含了实际传输的数据,如用户ID、角色等,签名用于验证令牌的完整性。 Java-JWT是一个用于处理JWTJava库。它提供了生成、解析和验证JWT的功能。你可以使用Java-JWT库轻松地在Java应用程序中实现JWT的生成和验证逻辑。 Java-JWT库的使用示例: 1. 添加依赖: ```xml <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.2</version> </dependency> ``` 2. 生成JWT: ```java import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; String secret = "your-secret-key"; String token = JWT.create() .withIssuer("your-issuer") .withSubject("your-subject") .sign(Algorithm.HMAC256(secret)); ``` 3. 验证和解析JWT: ```java import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; String secret = "your-secret-key"; String token = "your-jwt-token"; try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm) .withIssuer("your-issuer") .build(); DecodedJWT jwt = verifier.verify(token); // 获取载荷中的数据 String subject = jwt.getSubject(); // ... } catch (JWTVerificationException exception){ // 验证失败 // ... } ``` 以上是Java-JWT库的简单示例,它提供了一种方便的方式来生成、验证和解析JWT,帮助你在Java项目中使用JWT进行身份验证和授权。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值