问题描述
使用io.jsonwebtoken.Jwts构造了一个token,在解析这个token时,发现解析秘钥和构建秘钥不完全相同也可以成功解析,代码如下
签发token
@Test
public void testJwt() {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("888")
.setSubject("Rose")
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "xxxx");
String token = jwtBuilder.compact();
System.out.println(token);
String[] split = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
解析token
@Test
public void testParseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJSb3NlIiwiaWF0IjoxNjMyNzMxMzYyfQ.9GZJUoTNwJuMh5tFMd3giJh36YNuCnFuWsDKONOg2C8";
Claims claims = (Claims) Jwts.parser()
.setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println(claims);
}
在签发时我的秘钥是四个x,但是在解析时,4-7个x都能成功解析,我很不理解,虽然不是什么秘钥都可以成功解析,但是近似的就能解析也太不安全了吧
问题分析
我第一时间想到的是去网上查,没有想到第一时间自己看源码,因为可能源码一时半会看不出来个啥。去网上查了一下,果然查到了一些有用的东西,然后我点进源码看了一下,一下子就清楚了。
先查看设置秘钥的地方,io.jsonwebtoken.JwtBuilder有一个默认的实现类io.jsonwebtoken.impl.DefaultJwtBuilder,我们看看他的signWith方法,signWith有三个重载形式,我们只看我们调用的那个就行了

关键的地方我标出来了,signWith拿到秘钥之后是先将秘钥用base64解码之后再进行操作的。
然后再看解析token时的地方,io.jsonwebtoken.JwtParser也有一个默认的实现类io.jsonwebtoken.impl.DefaultJwtParser,设置秘钥的方法setSigningKey也有三个重载形式,我们使用的是下面这个

可以看到,setSigningKey方法也将拿到的秘钥用base64解码了,咋一看好像没什么问题,解码就解码呗,反正两边都解了,同一个字符串,解码过肯定是一样的结果,但问题恰巧就出现在这里。
虽然相同的字符串经过base64解码过后也一定是相同的,但是不同的字符串经过base64解码过后也可能是相同的!!
下面是我的测试

可以看到,我测试了三种不同地方的工具类,在将xxxx和xxxxx按照base64解码时,都解码成相同的byte数组了。在jwt设置秘钥和解析秘钥时,使用的是第一种,所以就不难理解,为啥秘钥即便不完全相同,只要近似也能解析。
解决问题
虽然搞清楚了原理,那怎么解决这个问题呢?或者这是jjwt的一个或base64的bug?是不是base64的bug我不知道,但是一定不是jjwt的bug,只是我们调用api是没有按照人家要求调用。再来看看构建token时设置秘钥的方法

人家方法的变量名叫:base64EncodedSecretKey,就是想让你传一个经过base64编码过后的字符串。接口上也有明确的注释:

在解析token时设置秘钥的方法也有同样的注释。如果按照api要求来调用,就没有问题了,因为base64是对称加密的,解码被加密过后一样的字符串,得到的肯定是两个一样原始内容。
反思总结
在调用api时,没有好好的看别人的文档或是注释,记大过一次。
联想拓展
我对base64了解没那么深,只知道是一个对称加密的算法,但是也会感到奇怪,为什么对不同的字符串进行解码时,能得到相同的结果呢?
我只能尝试这么理解:Base64在加密时,是通过一步一步加密的,相同的东西,在加密时经过的步骤都是相同的,所以最终加密出来也是相同的。但是解密则有点不同,什么不同呢?如果你是经过我Base64加密过后,你再来经过我解密,我能按照加密过程的逆过程,一步一步把你还原成原来的样子。但是如果你压根就没有经过我Base64加密,你却要来走我Base64的还原过程,那我在还原的过程中,有的步骤都没法走,直接跳过了,最后把你“还原”。所以那些近似的、不是经过Base64加密的字符串,在经过Base64解密时,都有一些相同步骤跳过了,所以最终解密过后得到了相同的东西。当然,这都是我自己的理解。
另外,在测试过程中,我发现,有的地方的Base64工具类,测试的结果不一样,上面测试了三个base64解码,分别是:
- io.jsonwebtoken.impl.TextCodec.BASE64
- io.jsonwebtoken.impl.Base64Codec
- cn.hutool.core.codec.Base64
三者在解密五个x时,结果都是一样的,也都和4个x的解密相同。但是在解密6个x和7个x时,就有所不同了

前两者解密6个x和7个x时,得到的结果依然和4个x是相同的,但是第三者解密出来是不同的,不知道这是为啥。
文章参考:如果SigningKey稍有不同,JJWT解析不会失败