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

每月一争, 为什么 JWT 这么多诟病, 什么下线设备登录 JWT 不是很容易解决吗?

  •  
  •   seth19960929 · 2023-06-19 12:13:04 +08:00 · 5631 次点击
    这是一个创建于 509 天前的主题,其中的信息可能已经有所发展或是发生改变。

    JWT 的基本就不再过多阐述

    思考的问题前提: 比如我们面对一个千万级用户的系统. * 客户端需要和服务端交互, 假设 50% 的接口需要校验 * 10% 的接口需要获取个人信息

    • 如果是 token 那么代表着 50% 的流量都需要去查 db(任何 db 都可以)
    • 如果用 jwt 我可以减少到 10%, 甚至更少

    至于常见的 JWT 诟病解决方案

    • 两个小时没操作自动掉线
      • 分钟级别过期 access_token, 2h 过期 refresh_token, 完全可以做到
    • 黑名单, 强制下线功能
      • 分钟即被过期 access_token, token 里包含信息 {uid: xxxx, token_version: xxx}
      • 修改密码, 或者强制下线, 修改 db 里的 token_version
      • 当要刷新 token 的时候, 校验客户端 token 里的 token_version 是否等于 db 里的 token_version, 不等于强制下线.(是在每一次去校验还是在刷新 token 的时候去校验都可以)
    • 单台设备下线, 这个我认为是业务的逻辑
      • 分钟即被过期 access_token, token 里包含信息 {uid: xxxx, device_id: xxx}
      • 当要刷新 token 的时候, 校验客户端黑名单 db 里是否包含这个 device_id

    为什么要用 JWT ?

    • 使用 token 会有千万级用户请求 * (存 token + 查 token redis)... 用户名,xxx 存储,

    下线, 黑名单为什么不直接存储 token ?

    • token 可能千万级别, 但是黑名单保守估计不会超过 1w, 并且我可以把黑名单 token 有效期设置成 refres_token 的有效期(极大的减少了黑名单 token 的数量), 这样子当黑名单过期了, 那么 refresh_token 也不能再使用了(如果去颁发, 在黑名单中也颁发不了)
    • 如果一个用户可能登录 5 个设备, 那么存储的 token 量就是 1000w * 5, 而 jwt 就是 0 ~ 黑名单数量
    第 1 条附言  ·  2023-06-19 13:50:10 +08:00

    使用网站: http://www.redis.cn/redis_memory/

    1000w key 占用量

    key 为 32 位字符串, value 如下(我按 128 个字节算):

    {
        "id": 999999,
        "nickname": "seth",
        "vip_exp": 1687153583,
        "avatar": "https://wwww.baidu.com/a.png"
    }
    

    网站得出的结果是: 2.4G,

    55 条回复    2023-10-07 17:50:07 +08:00
    seth19960929
        1
    seth19960929  
    OP
       2023-06-19 12:16:45 +08:00
    还有 access_token 一个就够了, 为什么还要有 refresh_token, 除了官方说的安全之外, 就是可以作为两个时间点去使用.
    access_token 每次都要校验, 可以简单的校验, refresh_token 使用的频次比较少, 这时候可以做更完整的校验.
    LeegoYih
        2
    LeegoYih  
       2023-06-19 12:42:44 +08:00
    用了 JWT 还要存 token 到服务端,每次还要查版本号和黑名单,不是脱裤子放屁吗?
    用户权限之类的更新了怎么办?难道让用户退出重新登录一遍吗?
    查一次缓存就能解决的事情,不要太小看 Redis 的性能了。

    而且 JWT 不是 100%安全的: https://github.com/yihleego/jwtcrack
    oldshensheep
        3
    oldshensheep  
       2023-06-19 12:53:10 +08:00
    你这个都依赖 **分钟级别过期 access_token**

    你加个 token_version 那每次刷新 jwt 就要查询数据库,而且你刷新的非常频繁,这不又和原来一样
    同样的还有检查 device_id 要查询数据库
    giter
        4
    giter  
       2023-06-19 12:56:45 +08:00
    我选择把 accessToken 与 refreshToken 存在 localStorage 中。既然要 JWT ,那就贯彻到底,给服务端彻底减负。
    nomagick
        5
    nomagick  
       2023-06-19 13:03:20 +08:00
    jwt 真正的用途在于验证其他站点的用户和 token ,而不是自己站点的。
    不要为了用而用。
    ztxcccc
        6
    ztxcccc  
       2023-06-19 13:08:16 +08:00
    别查库了谢谢
    amlee
        7
    amlee  
       2023-06-19 13:13:02 +08:00
    jwt 的设计初衷就是要无状态,如果你的需求必须要求有状态,那么 jwt 的设计不适合你,为什么不用现成的 session 方案?
    这本质上是一个需求和设计是否匹配的问题,搞清问题本质,跟技术优劣,或者说能否给技术打补丁以满足需求没关系。
    板砖和锤子都能敲钉子,你非要说我能打磨一个完美的板砖去敲钉子,那我只能说脑子有包
    gogola
        8
    gogola  
       2023-06-19 13:17:22 +08:00
    @LeegoYih #2
    暴力破解这玩意,不好说。
    CodeCodeStudy
        9
    CodeCodeStudy  
       2023-06-19 13:21:13 +08:00
    token 存 redis 不就完事了?
    emric
        10
    emric  
       2023-06-19 13:21:29 +08:00
    我这边用 last_login ,日期不相同就拒绝。
    感觉你说的大部分问题,都是修改以下 last_login 就行。
    seth19960929
        11
    seth19960929  
    OP
       2023-06-19 13:24:49 +08:00
    @LeegoYih 我就问一下, 你使用 token 权限更新了怎么办? 这个和 jwt 没关系. 你权限更新其它方式该怎么做就怎么做. 不是我小看, 而是如果还不消耗存储, 不消耗 io 的方式为什么不讨论呢?

    @oldshensheep token 的方式, 按用户级别来说, 每次请求来都查 redis, 并且 jwt 包含千万级别的用户数据, 而刷新的时候查询, 减少了 90% 的请求, 并且 redis 的存储只用非常少
    @giter 没错了, 那肯定都要存客户端的, 和我说的没区别
    @nomagick 你在哪看的定义? 你说的是 oauth2 吧
    @ztxcccc 举个栗子
    @amlee 又来一个不看解决方案, 上来就说不查库的
    seth19960929
        12
    seth19960929  
    OP
       2023-06-19 13:26:01 +08:00
    @CodeCodeStudy 话说不看场景直接就存?
    @emric 原理方案差不多的, 总是有些人在说为什么不存 redis 呢. 所以要讲这个东西.
    ellermister
        13
    ellermister  
       2023-06-19 13:33:45 +08:00 via Android
    那每个 jwt 请求到服务端不都是要查一下有没有在 redis 黑名单列表里,千万级的用户也是千万级的查询啊,jwt 只是把 redis 存储大小改了,没改变查询次数。
    @seth19960929
    seth19960929
        14
    seth19960929  
    OP
       2023-06-19 13:33:57 +08:00
    @LeegoYih 我看了两眼你这个代码, 上来来个碰撞代码就说不安全, 能拿出具体的文献出来吗.
    按你这样子说, 世界上没有安全的加密, 我只要写一个碰撞代码, 它们安全只是我现在没有碰撞到, 不代表我做不到.
    seth19960929
        15
    seth19960929  
    OP
       2023-06-19 13:39:22 +08:00
    @ellermister 嗯嗯, 这是一个讨论帖子.
    两种方案:
    1. 只在刷新 token 的时候才查询是否在黑名单, 请求可以降低非常多, 有个问题就是会有延迟, 比如 5 分钟刷新一次 token, 那么就会加入后有 5 分钟延迟. 如果是部分场景完全可以用这种(只要 app 左右逻辑处理, 能及时失效, 如果有人窃取了 token, 那么它也只能使用 5 分钟, 到期了刷新就会得知被加入黑名单)
    2. 大小还是差别很多的. 往 redis 里存 32 位字符串 key 1000w 个看看. 我等会给一个答案
    8355
        17
    8355  
       2023-06-19 14:00:50 +08:00
    只用 jwt 没有防御措施绝对是错误的行为

    单 ip 连续请求错误 token 值为不可信行为,这是风控策略应该做的事
    在敏感操作必要时进行密码检查

    仅传入 token 就给予最大用户操作权限本身就不合理
    wunonglin
        18
    wunonglin  
       2023-06-19 14:01:36 +08:00
    你说得对
    Masoud2023
        19
    Masoud2023  
       2023-06-19 14:04:19 +08:00
    你文中的黑名单和单台设备下线,归根结底都是要去查库,那用 jwt 和不用 jwt 做 sessionid 有什么区别么
    seth19960929
        20
    seth19960929  
    OP
       2023-06-19 14:05:57 +08:00
    @LeegoYih
    只要循环结束, 我一样能破解呀.

    for ($i = 1; $i < pow(32, 16); $i++) {
    $key = genKey($i);
    $result = openssl_decrypt(hex2bin('1fbf2605f954fad3ba18115000735aee'), 'aes-128-cbc', $key, 1, '0000000000000000');
    }
    seth19960929
        21
    seth19960929  
    OP
       2023-06-19 14:08:20 +08:00
    @8355 这个是,敏感操作已经和是 token 还是 jwt 没关系
    @wunonglin 你说得对
    @Masoud2023 sessionid 存取 1000w 数据, 没有哪家的黑名单有 1000w 吧? 并且我可以在刷新 token 的时候再查 redis, 而不是每次请求.
    nothingistrue
        22
    nothingistrue  
       2023-06-19 14:29:41 +08:00   ❤️ 1
    JWT 全称是 Javascript Web Token 。它与传统 Token 的唯一区别就是它采用非对称加密的方式。这有两个好处:一,可以自验证;二,可以携带更多的信息(传统 Token 采用 MD5 、SHA1 这样的散列加密方式,这是无法自验证和反解码的)。

    JWT 只是一个高级的 Token 而已,并不是会话 /用户跟踪方案,在会话跟踪方案上讨论 JWT 的优劣根本毫无意义。而 2 楼这种比较,是想把 Token 、会话跟踪、认证(用户)、授权(权限)糅合到一起去考虑,神仙看了都会躲。

    楼主说得实质上是 Token 采用 JWT 的会话跟踪方案。在会话跟踪方案上,用 JWT 代替普通 Token 、SessionId 等,除了开发难度略增之外,是只有好处没有坏处的。
    ellermister
        23
    ellermister  
       2023-06-19 14:33:40 +08:00
    @seth19960929
    5 分钟的延迟并不能满足常见的需求

    比如,假设我登录了一个用户,拿到 jwt token ,此时点击注销,服务端加入黑名单。然后登录过程中 token 泄露或者前端只是选择性从 localstorage 删除,本质意义上服务端还存在。

    因为黑名单查询不是即时的,那我就可以在服务端通过改密码方式修改用户的密码,然后重新登录,又可以拿到一个新的 token 。(当然有人会说改密码要二步验证之类的,这是另外一种做法和产品需求范畴了)

    > 总之在有效期内仍可以做很多事情,或者不能使其立即下线,就有很多的安全隐患。

    有人说你这个强依赖于状态, 不能用无状态做, 那可能确实。但目前的大多数产品提出的需求或者我做过的产品,基础都要满足我所说的即时性。(注销了这个 KEY 就无效了, 在任何层面上他能操作能认证就是一个 BUG )

    我看了楼上,思考过确实对用户的系统不适合做 jwt ,只适合真正意义上的无状态需求(下载站鉴权、cdn 鉴权之类的、这些太少了,我基本没接触过)。而只适合所谓第三方鉴权的,服务对 服务之间的鉴权。


    虽然我也在用 jwt 、而且每次请求都必命中 redis 查验黑名单,99%请求命中一次数据库。
    大部分接口都需要获取用户的自身信息,为什么用户信息不全部存到 jwt ?因为不及时不可靠。

    有人说你这样用 jwt 比传统 token 都麻烦,存储的还多几倍的黑名单还基本都要查 redis 、数据库,何必 jwt ,但 jwt 能够方便对接前端及第三方这种共识特性,迫使选择了 jwt 。

    总之一句话,我不觉得 jwt 很好,我也觉得一身诟病。我选择 jwt 唯一的技术优势就是它只存黑名单,而传统 token 存白名单。能省点存储就省点吧。
    fivesmallq
        24
    fivesmallq  
       2023-06-19 14:33:48 +08:00
    https://jwt.io

    JSON Web Token (JWT)
    @nothingistrue
    QlanQ
        25
    QlanQ  
       2023-06-19 15:48:03 +08:00
    我是彩笔

    如果多端,类似 pc + 小程序 + app + h5 想要统一一种认证方式 jwt 不是唯一选择吗?

    如果多服务系统,比如 主站 Java ,活动页 php ,im go 这种,用 session 不是更麻烦吗?
    hsfzxjy
        26
    hsfzxjy  
       2023-06-19 15:55:26 +08:00 via Android
    @nothingistrue 是 JSON Web Token
    mxT52CRuqR6o5
        27
    mxT52CRuqR6o5  
       2023-06-19 15:57:30 +08:00
    我十分怀疑你的基于 jwt 的登录业务设计的是否正确
    我没看明白强制下线功能为啥可以不是每次请求都请求 redis ,那你咋知道当前的 redis 中的 token_version 是多少
    是因为你的强制下线功能不是实时的,会有 5 分钟延迟吗?
    mxT52CRuqR6o5
        28
    mxT52CRuqR6o5  
       2023-06-19 15:59:06 +08:00
    @ellermister #23
    +1 ,安全的不完全等于完全的不安全,没见过哪家登录系统强制下线还有 5 分钟延迟的,5 分钟足够把账户里的钱偷光了
    hongfs
        29
    hongfs  
       2023-06-19 16:03:49 +08:00
    @QlanQ JWT 并不是唯一,而且可能会让复杂度变高。我们现在的多端方案,同一个接口(也可以不同接口),带上不同端的 type 过来,因为不同 type 提交的内容存在差异,后端返回的是一个随机生成的 Token ( 40 位字符串)存放于 Redis ,可以做到每个端都可以有一个在线,可以做到当修改密码或者拉黑时全部端都可以一起下线。
    mxT52CRuqR6o5
        30
    mxT52CRuqR6o5  
       2023-06-19 16:04:26 +08:00
    人家支付宝微信不比你的系统用户多多了,也没见人家用牺牲安全性的方法(强制踢下线有 5 分钟延迟)去提高系统性能
    QlanQ
        31
    QlanQ  
       2023-06-19 16:14:47 +08:00
    @hongfs 我不太能区分,jwt 和 token 的区别,在我看来原理和作用差不太多,只是说 jwt 有一个标准的规则,
    如果有这种黑名单,强制下线的需求,我会将 token 存在 redis 中

    然后多端登录,就是 redis ,key 的规则问题了,同样可以实现修改密码登录失效的需求
    justfindu
        32
    justfindu  
       2023-06-19 16:20:49 +08:00
    @QlanQ 没有什么区别, 因为它叫 JSON Web Token
    mmuggle
        33
    mmuggle  
       2023-06-19 16:45:41 +08:00   ❤️ 1
    JWT 本身无状态,黑名单功能就是强行有状态了,但是只是针对黑名单 token ,总体来说,还是比传统 token 强一点。
    seth19960929
        34
    seth19960929  
    OP
       2023-06-19 17:16:07 +08:00
    @nothingistrue 说的在理

    @ellermister 黑名单下线这个问题, 其实是一个取舍问题, 就是如果不能接受刷新 token 这段等待时间的话, 没什么好说的了, 就是每一次查询都走黑名单查询(其实很多系统第一次打开 app 或者重新登录才去校验), JWT 确实能省很多内存
    @QlanQ 嗯嗯, token 也是一种方式, 就是随机字符串存到 redis 映射出 uid, 现在说这种存 redis 和 jwt 哪种更好.
    @mxT52CRuqR6o5
    seth19960929
        35
    seth19960929  
    OP
       2023-06-19 17:18:08 +08:00
    @mxT52CRuqR6o5 你这个话说的, 如果是和钱相关, 那和 JWT 有什么关系, 这时候不应该发短信, 人脸识别一堆校验, 这时候全面风控早就拦截好了, 你用 JWT 做这个事不合适.
    seth19960929
        36
    seth19960929  
    OP
       2023-06-19 17:21:05 +08:00
    @QlanQ JWT 只是一个标准, 也是 token 的一种. JWT 黑名单用 redis 存的做法, 大家就会觉得为什么不直接把 token 直接存 redis 里.
    @mmuggle 1
    hongfs
        37
    hongfs  
       2023-06-19 17:55:39 +08:00
    @seth19960929 #36 对大多数业务来说,其实 Redis 的成本也是很低的,比如你计算的 1000W key 那会有 2.4G 的一个内存使用,1000W 又不是一直在线的用户,所以内存的实际占用也是非常小的。即使你的用户在线率非常多,你需要对 Redis 进行集群化的资源成本也是非常低的。

    如果非要 JWT ,其实可以做成两阶段的,用户请求先 JWT 验证,然后 sha1 等方式减少存储空间,把 sha1 的值去 redis 里面查。
    QlanQ
        38
    QlanQ  
       2023-06-19 17:56:32 +08:00
    @seth19960929 所以我说我分不清 jwt 和 token ,明明都是一个字符串而已,在我的认知里面,就是一个东西
    seth19960929
        39
    seth19960929  
    OP
       2023-06-19 18:07:45 +08:00
    @hongfs 如果 token 有效期一个月, 1000w 肯定有的. 如果是抖音那种日活亿级别的, 那得多少内存存 token
    @QlanQ 普通的 token 无任何有效信息, 只是一个唯一值, JWT 的话可以通过 秘钥 从 token 里解析出 uid 之类的, 这样子有些场景只有 uid 就不用查数据库.
    daimubai
        40
    daimubai  
       2023-06-19 18:14:10 +08:00
    如果存库的话,那确实 jwt 和普通的字符串没区别。因为最终都会查库的,就算不解析 token ,也能在库中查到当前用户的信息
    akira
        41
    akira  
       2023-06-19 18:18:29 +08:00
    那个,你们家用户都是千万级的了么。。。
    GeruzoniAnsasu
        42
    GeruzoniAnsasu  
       2023-06-19 18:49:57 +08:00   ❤️ 4
    看了半天在吵什么……

    > 普通的 token 无任何有效信息, 只是一个唯一值, JWT 的话可以通过 秘钥 从 token 里解析出 uid 之类的, 这样子有些场景只有 uid 就不用查数据库
    原来还就是车轱辘话翻来覆去讲,说到底 OP 发现的「优势」也就是 jwt 带信息,随机 token 不带信息而已,这不就是废话……


    > 至于常见的 JWT 诟病解决方案
    OP 的这段发散了这么多,加上他楼下的回复,其实就一句话: 退化成普通 session / token 方案


    > 为什么要用 JWT ?
    > - 使用 token 会有千万级用户请求 * (存 token + 查 token redis)... 用户名,xxx 存储,
    文不达意。 根本原因其实就是 jwt 携带的信息可以短路一部分查库判断而已




    我来替 OP 总结一下他的看法:
    1. 在最坏场景下,jwt 退化成普通 session/token 方案,此时主要缺陷是 jwt 数据量引入的内存浪费,但即使在极大并发量条件下 redis 的性能也完全够用,所以可以忽略不计
    2. 在典型场景下,jwt 自带 session/token 方案需要查库才能获得的关联信息,如用户 id 等,在这些场合下能节省大量数据库连接资源


    所以这不就是什么「初识」、「入门」文章里就提到的东西么……
    hyperbin
        43
    hyperbin  
       2023-06-20 07:54:41 +08:00 via Android
    @LeegoYih If you are very lucky or have a huge computing power ,相当于说国库不安全,如果你有灭国级的实力的话
    seth19960929
        44
    seth19960929  
    OP
       2023-06-20 08:57:09 +08:00 via Android
    @daimubai 查一次和一万次能一样吗?这样子说的话,干嘛架构这么多,干嘛用这么多缓存,直接查数据库了
    @GeruzoniAnsasu 一直强调更多的怎么去解决 jwt 的问题,我后面说的这么多黑名单,刷新的机智话说都无视了吗
    @akira 这是一个讨论贴,可以直接 block
    QlanQ
        45
    QlanQ  
       2023-06-20 09:01:02 +08:00
    @seth19960929
    嗯嗯, token 也是一种方式, 就是随机字符串存到 redis 映射出 uid, 现在说这种存 redis 和 jwt 哪种更好.

    本质都是一个字符串是吧,jwt 、token 傻傻分不清
    seth19960929
        46
    seth19960929  
    OP
       2023-06-20 09:45:56 +08:00
    @QlanQ 对, json web token 从名字看出来它也是一种 token
    daimubai
        47
    daimubai  
       2023-06-20 11:16:51 +08:00
    @seth19960929 #44 查一万次是怎么得来的?
    daimubai
        48
    daimubai  
       2023-06-20 11:20:52 +08:00
    @GeruzoniAnsasu 他都不知道自己在说些什么
    chendy
        49
    chendy  
       2023-06-20 11:54:04 +08:00
    JWT 适合自家服务之间通信,网关授权之后带上 JWT 请求后续服务,后续服务不需要再查用户权限之类的东西,降低用户中心服务的负载
    客户端还是适合最朴素的 token 模式( cookie 里的 sessionid 其实也是一种 token )
    seth19960929
        50
    seth19960929  
    OP
       2023-06-20 14:09:48 +08:00
    @daimubai 1000w 用户都请求有没有 1000w 次, 什么怎么得来的.
    @chendy 就网上的帖子来看, JWT 很多时候还是给客户端用的, 而不是你说的内部服务
    v2Geeker
        51
    v2Geeker  
       2023-06-21 09:20:56 +08:00 via iPhone
    我们公司是全线用的 OIDC 那一套,当然也就用了 JWT 。这套像介于传统 Session 和 无状态 JWT 之间,服务端和客户端双赢。
    chenlins
        52
    chenlins  
       2023-07-04 22:56:28 +08:00 via iPhone
    @chendy 赞同,我觉得这是 JWT 的最佳实践
    8rmEHZ8WhVHVOb0E
        53
    8rmEHZ8WhVHVOb0E  
       2023-10-07 00:34:43 +08:00
    你说的方案并没有节省 Redis 查询次数,只省略了 redis 内存而已,但是:
    Jwt 方案 token 长度往往有数百个字符串,并且根据 Payload 内容的增多而变的更长,每次请求都会把这些东西带上。
    而类 session 模型的 token ,token 字符串长度往往控制在几十个字符串长度。

    假设每个平均每个 jwt token 长度 250 个字符串,而 session 模式的 token 长度 50 个字符串,那么每个请求就多了 200 个字符串,如果你的网站每秒 1 万 QPS ,你的服务器带宽峰值大约会提高 40Mb

    40Mb 带宽明显比几 G redis 内存要贵鸭
    seth19960929
        54
    seth19960929  
    OP
       2023-10-07 17:49:01 +08:00
    @xiaomada 服务器的带宽一般只计传输给客户端的, 不计客户端上传
    seth19960929
        55
    seth19960929  
    OP
       2023-10-07 17:50:07 +08:00
    @xiaomada 请看原文, 只有当 refresh 的时候才去查询 redis. 至少减少 99% 的查询 redis 次数.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2620 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 02:58 · PVG 10:58 · LAX 18:58 · JFK 21:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.