V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
LeeReamond
V2EX  ›  问与答

有关 JWT 能否真正实现无状态

  •  
  •   LeeReamond · 2021-03-03 10:12:36 +08:00 · 1622 次点击
    这是一个创建于 1368 天前的主题,其中的信息可能已经有所发展或是发生改变。

    关联贴 https://v2ex.com/t/757713#reply20

    看到楼里#37 老哥的讨论,很有收获,开个新帖说一下自己的理解,不知道对不对,大家轻喷。

    之前跟群里老哥讨论,老哥说你这个做到最后会发现还是有状态的,我感觉则不是,我感觉按这个思路 jwt 是可以真正实现无状态的。简单来说我的逻辑是,jwt 唯一功能是保证 header 合法,合法的 header 里可能包含过期内容,这个 jwt 管不着。

    但是通过建立黑名单的方式,可以管理 header 是否合法。也就是说合法的 jwt 保证 header 合法,header 又不在黑名单上,那说明 header 的一切都是对的,即使用户说我是超级管理员,那你说是那就是,不需要回源进行校验。

    =====================================================

    假设一个简单的业务场景,某用户 A 在第一天注册账号,在第二天手机丢失,理所当然地连带 jwt 一起丢失了,这时候他希望紧急改密码,防止损失扩散。

    说一下个人理解的具体实现方法,为了方便理解假设全部 content 都使用明文,以“中心服务器”指代压力最后的数据节点,这个数据节点一直持有同步最新且正确的状态。

    首先 A 注册并登录账号,过程中必然要使用到username=admin,password=12345678的传输,业务节点也必然要请求数据端。请求验证通过后,服务端给 A 回复一个 header,内容为

    {
        username = admin
        authlevel = 9
        passwrod = 1234    //   
    }
    

    密码放在 headers 里肯定要经过反复的单向 hash 与截断保证各级别的原始内容不泄露。

    然后次日 A 的手机丢失,A 修改密码,此过程中中心服务器由它的请求附带 header 得知了要屏蔽的username+password组合为admin+1234,由此希望的行为是所有业务节点遇到该组合的 headers 时,拦截其请求。

    那么中心服务器当然要维护一个黑名单,由于目前只有一个人改密码,所以黑名单长度为 1,内容为字符串 admin+1234, 并且同时中心服务器向所有业务节点分发一个状态机,这个状态机生成的过程是这样:

    使用一些快速哈希算法,比如 fnv1-32/fnv1a-32/fnv1-64/fnv1a-64,
    每个算法生成一个 mod512 的结果(对应即每种算法有 512 种可能的结果)
    admin+1234 的对应结果为( 142/296/302/40 )
    
    根据 Bloom Filter 原则,状态采用比特位累加,512 种可能性要采用 512bit 表示,
    换算后正好为一个 64byte 的字符串,是 sha256 的原始长度,我们用四个这种长度的字符串即可表达该 hash 组合的对应结果。
    
    针对目前这个特殊情况,这个长度为 256 的字符串中,18 位为'\x20',101 位为'\x80',166 位为'\x08',192 位为'\x80',其余全部为`\x00`
    

    中心服务器同步状态后分发这个长度为 256bytes 的字符串,而从此以后业务节点在所有鉴权行为中,对 jwt 解码后,都要根据状态机校验他们的 user+pass 是否符合这个黑名单状态,如果不符合说明不在黑名单中,则 jwt 的内容可以完全信任,即便其中包含一些敏感信息,比如权限级别等等。这样避免了回源请求的开销。如果符合黑名单,那么仍有误判可能,回源进行请求。但由于 Bloom Filter 长度为 2048,在黑名单内容较少的条件下可以做到碰撞概率极低,黑名单内容较长的情况下可以做到直接校验通过大部分请求。由此可以缩减绝大部分业务节点回源请求的数量。

    ================================================

    这样基本上实现了 jwt 设计的初衷即业务端点可以横向扩展而数据节点压力基本不会怎么增加。整个过程中后端同步状态的通信只发生在用户信息加入黑名单,和用户信息离开黑名单的时候,全局同步共两次,同步内容是长度为 256bytes 的字符串。

    另外有关黑名单,由于 jwt 的 secret 有刷新机制,假设我们业务节点的 jwt 每半月刷新,即单个 jwttoken 的最大有效期为一个月。那么事实上黑名单只需要维护有效期为一个月即可。

    以上只是一些各人构想,大家轻喷。v2 朋友知道我是做 pythonweb 技术栈的,在寻找 hash 算法过程中在 pypi 没有找到,所以顺带实现了一个 c 的版本,过两天放到 pypi 上。我本机测试延迟的话,这个额外 hash 校验的开销是比较低的,py 整个调用的延迟大概在几十纳秒的水平,可以认为其阻塞不对 python 的异步服务产生任何影响。

    第 1 条附言  ·  2021-03-03 10:53:23 +08:00

    进一步测试了一下,帖子中的例子里的碰撞体积太小,mod512的话,预先加入1000个随机黑名单值,之后测试十万次随机输入,拦截率为 56% ,即二分之一的请求要回源,基本不具备使用可行性

    将bloomfilter进一步增大到mode1024,(以下认为随机输入重复率极低) 预先加入1000个随机黑名单值,之后测试十万次随机输入,拦截率为 15.3% 预先加入5000个随机黑名单值,拦截率为96%

    增大到mode2048, 预先加入1000个随机黑名单值,之后测试十万次随机输入,拦截率为 2.3% 预先加入5000个随机黑名单值,拦截率为48%

    增大到mode4096, 预先加入1000个随机黑名单值,之后测试十万次随机输入,拦截率为 0.26% 预先加入5000个随机黑名单值,拦截率为25% 预先加入10000个随机黑名单值,拦截率为68%

    增大到mode8192, 预先加入1000个随机黑名单值,之后测试十万次随机输入,拦截率为 0.0% 预先加入5000个随机黑名单值,拦截率为4% 预先加入10000个随机黑名单值,拦截率为25.8%

    增加到这么多的话,单次同步状态需要同步4KB文件,感觉同步开销可能也逐渐变大了?

    9 条回复    2021-03-03 11:22:40 +08:00
    also24
        1
    also24  
       2021-03-03 10:15:22 +08:00
    『中心服务器同步状态后分发这个长度为 256bytes 的字符串』

    这个过程是 Server Side 的。
    also24
        2
    also24  
       2021-03-03 10:29:14 +08:00   ❤️ 3
    以 JWT 自身的设计逻辑来说,靠的是『算法』验证,而非『数据』验证,这注定是一种『离线验证』方式。

    各类试图为 JWT 加入吊销机制的方式,实际上都需要引入『数据』,不管这个『数据』是什么形式存在的,都势必变为『在线验证』方式。
    各类优化的本质,还是将『数据』做压缩和分发,尽可能让这个『数据』去中心化。

    这个方式在现实中也许可以使用,也确实有了性能上的优化,但确实不是那个『离线验证』的 JWT 了,只能说是在 JWT 外,额外添加了一重黑名单机制,是两种验证方式的组合。
    coosir
        3
    coosir  
       2021-03-03 10:42:11 +08:00   ❤️ 2
    同意楼上
    如果业务场景想要主动让 JWT 失效,JWT 就没有明显优势吧,不就得每次都要校验白 /黑名单吗?
    哦,每次访问白名单判定是状态维持,每次访问黑名单判定就不是状态维持?
    lscexpress
        4
    lscexpress  
       2021-03-03 10:43:47 +08:00
    jwt 是理想的一个理论,适用于什么地方呢,适用于去中心化的地方。现实中,jwt+session 才是主流
    LeeReamond
        5
    LeeReamond  
    OP
       2021-03-03 10:54:49 +08:00
    @coosir 不纠结于学理上如何判断,只讨论工业部署方案的好与坏,黑白名单肯定是有区别的
    SjwNo1
        6
    SjwNo1  
       2021-03-03 11:05:56 +08:00
    无法实现 主动失效貌似只能依赖 database
    FreeEx
        7
    FreeEx  
       2021-03-03 11:16:32 +08:00
    如果用 [JWT] 和 [token] 做一些常见需求:
    1. 强制用户下线: [JWT] 黑名单, [token] 从缓存中移除。
    2. 统计用户在线状态: [JWT] ?, [token] 遍历 token 或者从数据库查询用户在线状态。
    3. 统计用户在线会话(终端)数量: [JWT] ?, [token] 从数据库查询用户会话列表。
    4. 限制用户在线会话(终端)数量: [JWT] ?, [token] 从数据库查询用户会话列表,移除时间较早的 token 。

    注:JWT 不能从数据库查询的原因是后端不知道 JWT 什么时候失效,用延迟消息的话又背离了服务端不存储的初衷。
    baiyi
        8
    baiyi  
       2021-03-03 11:20:01 +08:00
    同意二楼

    一旦你想要吊销 JWT,那么此时 JWT 的作用就不再能提供一个完整的授权,而是仅仅是通过签名成为一段不可伪造的信息,然后使用其他方法鉴权,这个时候它的作用与 session 没有本质区别。
    CoreJa
        9
    CoreJa  
       2021-03-03 11:22:40 +08:00
    2 楼说的好 @also24

    jwt 本来设计之初就是无状态的= =,就是"算法验证而非数据验证",是因为有用户下线需求这些,才搞出了 refresh token,token 黑名单这些。(这些都需要靠数据验证)
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   985 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 20:02 · PVG 04:02 · LAX 12:02 · JFK 15:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.