前因
愉快的周一(bushi,同时也是入职的第三周,本来还想着继续愉快的摸鱼,因为实在没有业务线能让我继续参与,但今天突然收到了 leader 的任务,在一个.net 的老系统的基础上,能够用 Spring 继续开发新的 CRUD,不就是换个铲子做农民工吗,难不倒我练习 CRUD 两年半的哥哥。这里讲一下我具体的任务,相信看完解决思路以后可能会对你有一点不一样的启发,那真是我荣幸之至。
任务
公司目前使用 .net 作为后端开发接口,因为后续业务线原因,需要本人,对!就是本人用 Java 开发一套新的服务,并能够和老系统无缝对接。乍一听好像不难,来我仔细给你捋捋,请继续往下看,有大坑!
- 老系统在登录时会返回前端 token,这个 token 是由 .net 中的 jwt 工具生成。
- 前端储存 token,并在后续受保护的接口请求 Header 中携带 token
- 我的任务: 在 SpringBoot 中使用 jjwt 库去验证这个 token,如果合法则继续执行业务代码,反之抛出验证错误异常
到这里应该没任何困难吧啦~
有聪明可爱一胎拔个的宝宝举手说了:「帅帅,你最难的一步就是跪下来求求你的 leader 把生成 token 时的 key 扔你脸上就行了,生成 jwt 和验证 jwt 应该是和平台无关的」
帅帅:你不说我一个月之后也能想到,哼(傲娇
到此为止,我快速的露出了以下代码(脱敏):
java
String key = "loveueveryday"
String token = "eyfdskhjfksdhkfj.pociopsahfhue.dsafhshfj" // 猫在键盘上画的
try {
Claims claims = Jwts
.parser()
.setSigningKey(key) // 设置签名时的 key
.parseClaimsJws(token) // 设置需要验证的 token
.getBody();
} catch (Exception e) {
log.error("token 验证失败");
}
我自信的按下 Run 按钮,举起茶杯,等待着 Terminal 的成功绿色,看着跳动的代码,轻哼着: 就这,不难,不费力,不费心,不对劲!!! 怎么爆红了喂,异常显示如下:
java
io.jsonwebtoken.SignatureException: Unsupported signature algorithm 'http://www.w3.org/2001/04/xmldsig-more#hmac-sha256'
我的额头出现微汗,开启了专注模式,解决不了这个问题的话,第二天就要因为左脚先跨进公司被劝退了呜呜呜
定位问题
今天主角: 很多项目(若依等) jwt 生成与验证的库: JJWT
一个小技巧,如果你的代码出现了意料之外的 bug,先不要着急,点进异常堆栈最近的一个去看看源码,看看异常是怎么被抛出来的,有的同学可能对源码存在一种天生的畏惧心理,想着大佬写的源码岂是我等凡夫俗子能读懂的?其实没必要,跟着我的脚步,你会发现源码其实也就是那么一回事,你我这样的普通人甚至可以修改源码来定制功能,只是使用的人多了,确保了这些轮子不会轻易出现问题。
- 跟进 jjwt 抛出异常的源码后发现:
java
public enum SignatureAlgorithm {
NONE("none", "No digital signature or MAC performed", "None", null, false),
/** JWA algorithm name for {@code HMAC using SHA-256} */
HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),
/** JWA algorithm name for {@code HMAC using SHA-384} */
HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true),
...
// 在枚举中寻找是否存在 header 中的 alg 算法类型,存在则直接返回
public static SignatureAlgorithm forName(String value) throws SignatureException {
for (SignatureAlgorithm alg : values()) {
if (alg.getValue().equalsIgnoreCase(value)) {
return alg;
}
}
// 注意看这个异常,不就是上面抛出来的吗?
throw new SignatureException("Unsupported signature algorithm '" + value + "'");
}
}
现在大概有一点思路了,可能是 C# 生成 jwt 时写进 header 里的 alg 算法,在 JJWT 这个库中找不到? 为了避免有人对 jwt 的部分概念不理解,这里稍微提一下哈~
偷窥 JWT
一个 JWT 分为三个部分: 1. header 2. payload 3. signature.
你有没有疑惑过,服务器是怎么确定客户端传来的 jwt 的可靠的呢?跟着我:
- 服务端生成 jwt 时需要一个 key,这个 key 是绝对保密的,绝对不能泄漏
- 登录时:服务端将组装好的 payload(通常是标识用户唯一身份字段: userId等),和需要用到的签名算法(常用 HS256,记住,后面要考),将签名算法以
alg: HS256
的键值对形式放进 header 中,再用 key + 签名算法生成出一个签名出来,也就是上面的 signature - 接口调用时: 服务端取出 token,将 payload 和 header 又拿去签名一次,得到验证时的签名。将验证时的签名与 token 中的签名一对比,如果一模一样,则说明这个 token 是可靠的,可以进行后面的业务;如果不同就有意思了,可能是某个小黑篡改了 token 中的值,但不知道服务端的 key,又不能篡改 token 中的签名
这下你对 token 的全流程有点概念了吧?那我们继续:cry:
回到主线
我们能轻易的发现: C# 生成 jwt 的算法好像也是 HS256?你仔细看看http://www.w3.org/2001/04/xmldsig-more#hmac-sha256
最后缩写是不是 HS256?Google 了一下还真是,那么目前大概就清晰起来了:
C# 生成 jwt 也是使用的 HS256 签名算法,并且这个算法 JJWT 是支持的。不过由于可能由于不同库的规范的原因,在 C# 和 JJWT 之间,header 中的 alg 字段就有了差异,下面看一看对比:
json// C# header: { "alg": "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", "typ": "JWT" } // JJWT header: { "alg": "HS256", "typ": "JWT" }
那现在问题知道了,应该怎么解决呢?
定制 JJWT 源码
首先,对于开源轮子,是不建议遇到问题一上来就修改源码的,最佳的是通过轮子提供的扩展点达到你要实现的功能目的,那为什么我要在 JJWT 的源码上动手脚呢?是因为我看了 JJWT 的源码后,发现验证 token 时是不支持自定义算法的。换句话说,如果 header 中 alg 的值在上面的算法枚举找不到的话,就会抛出异常。即使大家都知道这就是 HS256 签名算法,谁管你规范不规范的问题(如果有哥哥指出这个扩展点的话,直接说,不要担心我会哭
修改源码目的:
让 JJWT 能够知道 http://www.w3.org/2001/04/xmldsig-more#hmac-sha256
其实就是 HS256
,直接用他签名就是了,不要给我抛出什么异常。
步骤:
- 拉取 jjwt 源码
git clone https://github.com/jwtk/jjwt.git
- 切换指定版本
git checkout 0.9.1
-
安装依赖
-
定位目标代码(其实只需在上面的枚举类中添加一个枚举就行,看我操作)
java
public enum SignatureAlgorithm {
// JWA name for {@code No digital signature or MAC performed} */
NONE("none", "No digital signature or MAC performed", "None", null, false),
// JWA algorithm name for {@code HMAC using SHA-256} */
HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),
// 添加的枚举,将其与 HS256 进行映射
HS256_FOR_CS("http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),
...
public static SignatureAlgorithm forName(String value) throws SignatureException {
// 这时就能成功找到对应的算法了
for (SignatureAlgorithm alg : values()) {
if (alg.getValue().equalsIgnoreCase(value)) {
return alg;
}
}
throw new SignatureException("Unsupported signature algorithm '" + value + "'");
}
}
编译 打包 测试 成功!
文末
可以看到修改源码只有一小部分,最想表达的其实是定位问题到解决问题的思路。最后,如果对于文章有更好的建议,请指出,感谢您的时间,如果对你有所启发,将是我的荣幸之至。