JWT官网https://jwt.io/introduction/
参考
https://www.jianshu.com/p/a2efb2c8dcde
https://segmentfault.com/a/1190000009981879
https://www.jianshu.com/p/606cc5f0b936
https://www.jianshu.com/p/d1644e281250
https://learnku.com/articles/17883
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库或缓存。
jwt有3个组成部分,分别是
{"alg":"HS256","typ":"JWT"}
承载消息具体内容的地方 {"sub":"1234567890","name":"John Doe","iat":1516239022}
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
(我的密钥)
)
一个 token 一般来说有三个时间属性,其配置都在 config/jwt.php 内。
有效时间指的的是你获得 token 后,在多少时间内可以凭这个 token 去获取内容,逾时无效。
// 单位:分钟
'ttl' => env('JWT_TTL', 60)
刷新时间指的是在这个时间内可以凭旧 token 换取一个新 token 。例如 token 有效时间为 60 分钟,刷新时间为 20160 分钟,在 60 分钟内可以通过这个 token 获取新 token ,但是超过 60 分钟是不可以的,然后你可以一直循环获取,直到总时间超过 20160 分钟,不能再获取。
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160)
宽限时间是为了解决并发请求的问题,假如宽限时间为 0s ,那么在新旧 token 交接的时候,并发请求就会出错,所以需要设定一个宽限时间,在宽限时间内,旧 token 仍然能够正常使用。
// 宽限时间需要开启黑名单(默认是开启的),黑名单保证过期token不可再用,最好打开
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true)
// 设定宽限时间,单位:秒
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 60)
既然主要使用场景是鉴权,那么安全问题就是不得不考虑的问题了。下面对 JWT 可能需要的安全问题都进行一次深入的探讨并寻求最佳的解决方案。
客户端主动注销
客户端直接删掉这个token,再申请一个。然后就有了个可以用但是没人用的token。
进化:加一个注销,获取的同时注销上一个。注销的就加入黑名单,不让使用。(不会过大,过期了就可以移除黑名单了)
服务端主动注销 \ 用户修改密码
给每个用户分配一个secret,也就是一个用户只有一个token可以用。但是这就失去了意义,还不如维护session。
把要注销的用户的 uuid 和当前时间(TIME) 组成 key-value 对加入预黑名单,下次请求来时,若其 uuid 和黑名单中的对应,并且签发时间在 TIME 之前,则将其注销。这样查找范围就是未过期但又要注销的用户。并且在实现逻辑上这个预黑名单可以和签名的黑名单做到一起。
有人可能会觉得黑名单也是一种状态,用这种策略实现的 JWT 并不能算纯正的无状态。这种说法没错,但是考虑每次要检索的数据范围可以得到下面一个关系:
未过期但要提前注销的用户或 token 数 < 所有已登录用户数 < 所有用户数
此处的『 < 』基本可以看成『远远小于』,所以黑名单策略虽然也算有状态,但是其维护的状态数也是特别小的。
可见 『黑名单』策略能够有效解决 JWT 的注销问题。
session 可以自动续签,那 token 如何实现自动续签呢?我们先仔细分析一下在 web 和 app 环境中,token 分别如何续签。先具体分析 web 续签和 app 续签分别是什么样的具体需求。
那这个需求可以如何实现呢?
web
假设一个 token 的签发时间为 12:00,需求为 2h 未进行请求就要重新登录。则过期时间为 1h,刷新时间为 3h。
那么在 12:00 - 13:00 其都是可以正常使用的,如果在 13:00 - 15:00 进行请求,服务端自动换一个新 token 给客户端,达成续签。
如果 13:00 -15:00 之间没有进行请求,而是在 15:00 之后进行的请求,那么判断过期,需重新登录。
这样的话,最终的实现效果是:token 过期 2h 后需要重新登录 ,而不是 token 2h 未使用需要重新登录,导致的结果是,用户是 2 - 3h 未进行请求,需要重新登录。比设定的需求要多一个小时的不确定时间,但这也是没办法的办法了,至于会不会对业务造成影响,看具体需求吧,大多数的情况还是不会的。
app
和 web 端类似,设置成更长的时间周期即可。
对使用 Laravel 开发并使用 tymon/jwt-auth 这个插件的开发者,有个必须要注意的地方。
此处进行 token 的刷新并不是通过 refresh 这个操作获得新 token,因为这样 token 在不断的刷新过程中会达到一个刷新时间的上限。而上面的逻辑是每次都新签发一个 token,只要不断签就能够一直使用下去。 然后这里的旧 token 放入黑名单,黑名单有效期设置为『刷新时间』—— 3h。
当然如果开发者觉得这样不断签就能够一直使用不太好,那就可以设置更长的刷新时间,用 refresh 操作来获取新 token,刷新时间保证每次登陆得到 token 后,即使每次及时续签,最终也不会超过刷新时间。
然后这里又会出现一个新坑:
如果刷新时间设置为 14 天,过期时间设置为 2h。
token A 在 『 <= 14 天 』时刷新得到 token B,此时若再拿 token A 去请求刷新,肯定是不允许,否则 token 会出现『 1 变 N 』的问题,所以显然必须设置一个黑名单去放这些已过期但是又已经刷新过的 token。而这个黑名单的有效期范围应当为 token 的刷新期,即 14 天。然后你会发现对于每个用户每次登陆,需要维护的黑名单 token 数目最大可达 14 * 24 / 2 = 168 个,黑名单变得很大。
所以,如果要使用 refresh 操作,刷新时间务必是过期时间的尽量小的倍数。
web
假设一个 token 的签发时间为 12:00,需求为 2h 未进行请求即过期。则设置有效期 2h,不需要设置刷新期。那么每次请求都会把一个 token 换成一个新 token。如果 2h 没有进行请求,那么上一次请求的到的 token 就会过期,需要重新登录。同样是不断签就能一直使用下去。
如果想要和上面一样,不希望永久续签,则设置一个刷新时间即可。这个刷新时间不会导致进一步膨胀。
app
和 web 端类似,设置更长时间即可。
然后又到了问题时间:
每次都刷新 token,带来的性能影响如何?
以前每次请求,需要进行一次 token 签名校验,而现在是要签发一个新 token,进行的都是一次签名运算,那么运算量即从 n 变成 2n。
其次,每次刷新都要把旧 token 加入黑名单,会导致黑名单特别大,远远比方式一的设置刷新期大。
每次都刷新 token,并发请求时会不会因为 token 刷新而导致只有一个请求成功?
答案是确实会导致这个问题,怎么解决呢?设置一个宽限时间,每次 token 刷新后,原来逻辑应该是立刻不可用,现在设置一个宽限时间,让其在 n 秒之内仍然可用即可。
总之,这种策略会导致花费的 CPU 运算翻倍,并导致巨大的黑名单,然后必须设置一个宽限时间以解决并发请求问题,至于宽限时间会不会带来安全问题,微乎其微吧。
上面讲到,对于方式一【限定不能一直续签】,会导致巨大的黑名单,对于方式二,总会导致一个更加巨大的黑名单。那有没有解决方案呢?当然是有的。
我们可以这么想,既然一个 token 进行了刷新,那么签发时间在这次刷新之前的即可认为无效。于是,和上面的『预黑名单』策略类似,我刷新时不是把一个 token 加入黑名单,而是把 uuid-refresh_time 组成 key-vakue 对加入黑名单,这样针对每个用户的每次登陆,要存储到黑名单中的条目数就从 N 个变成了一个。
但是这样还要考虑一个问题:就是一个用户开两个浏览器,在不同的时刻在同一个系统都登陆了(假设业务允许),那么一个浏览器的 token 刷新就可能会导致另一个浏览器登陆失效。所以存储在黑名单中的 key-value 应该再加一个 key 以代表每次登陆,并且这个 key 要在 JWT 的载荷中随着刷新一直传承。
基于以上的优化,黑名单的大小变成了:每个用户同时登陆的系统个数之和,就变的和 cookie + session 一样了。
比如,A 系统(假设 2h 过期时间,14 天刷新时间),你用一个浏览器登陆了你的账号,我用 Chrome 浏览器登陆了我的账号,然后我又用 QQ 浏览器再登陆我的账号,那么黑名单的大小就为 : 1 + 2 = 3
而对于方式一【限定不能一直续签】,黑名单的大小(最大):168 + 168 * 2
而对于方式二,黑名单的大小为:你在 2h 内请求的次数 x ,我在 Chrome 浏览器请求的次数 y,我在 QQ 浏览器请求的次数 z 之和,即:x + y + z
如果要解决续签问题,方式一【可以一直续签】是个比较好的解决方案,虽然会带来一点小问题,但是并不会有太大的影响。方式二【限定不能一直续签】和 每次刷新会让黑名单的维护量和有状态差不多,但是有更高的安全性。
推荐:
https://learnku.com/articles/10885/full-use-of-jwt
https://learnku.com/articles/10889/detailed-implementation-of-jwt-extensions