V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
SSang
V2EX  ›  程序员

长期的用户令牌是如何存储的呢?存储结构是什么样的?

  •  
  •   SSang · 2022-04-20 18:00:01 +08:00 · 2250 次点击
    这是一个创建于 708 天前的主题,其中的信息可能已经有所发展或是发生改变。

    长期的用户令牌是如何存储的呢?存储结构是什么样的?

    背景

    一般来说 session 存储可以只生成一个随机 sessionId ,存在缓存中,value 是用户信息,返回给客户端可能是一个 cookie 里面的 token 之类的或者就叫 sessionId ,需要鉴权时候先拿 sessionId 找用户信息,再拿用户信息判断权限。

    这种对于长期的令牌看起来就没有那么友好,如果申请一个令牌存在数据库内,然后直接对着这个令牌赋权,那数据库泄露就寄了,一般密码还会 md5 或者 hash 一下存,但 token 完全是个随机值,hash 一下就找不到了,那么 token 直接明文存看起来比密码还不安全,虽然 token 泄漏和密码泄漏危险性也不一致,但总觉的不太合适。

    这就引出一个问题,是否要在 token 里面带上用户信息?

    方法 1:token 里面带用户信息

    比如我 token 由用户名+token 名+secret 组成,存储时候存 用户名,token 名,md5(secret),查询时候先把 token 解析出用户名和 token 名,然后跟查密码一样确认 token 是否合法。

    想讨论的问题

    这个方法看起来没什么问题,相当于带上用户名和密码来访问了,不过这方法就需要各端协商出一个统一的解析方式。

    这其实很容易想到 JWT ,可能这里直接用 JWT 就好了,还能帮忙做防篡改等措施,不过 JWT 设计的正常用法是无状态的,他会带来一个吊销困难的问题,感觉也很少长期 token 的设计会使用 JWT 的,而如果我把 JWT 有状态的使用看起来就有点怪。现实情况是否有人拿 JWT 做有状态 token 的呢?或者是否有其他比较好的方法吗?

    方法 2:用户传 token + 用户信息

    还有种方式是,把 token 就当作密码,每次登录时候用 token 代替密码登录,比如 git push 时候用的 token 就很像。

    想讨论的问题

    不过他实际存储时候是否是明文存储呢?如果是用的 hash 值存储,那如果一个用户有多个 token ,那是不是就得校验多次呢?并且 token 还能细分权限比如 api 的读写区分,还有项目 token ,ci token 等等,token 多起来之后索引是如何解决的呢

    第 1 条附言  ·  2022-06-08 15:21:42 +08:00
    问题应该是长期 token 是否可以使用明文存储?

    结论是,可以,且只能用明文存储

    因为加密存储会导致索引问题,这个问题无解,因为不像密码一样,可以通过用户名去索引。

    只要 token 作为 key 就永远无法防止泄漏风险,多一步加密就是掩耳盗铃,徒增索引负担。

    所以长期令牌应该就只要 token 作为 key ,value 存用户信息即可。安全问题通过权限细分和数据库访问安全来保证
    Chad0000
        1
    Chad0000  
       2022-04-20 18:08:27 +08:00   ❤️ 1
    生成两个 Token ,一个叫 RefreshToken 长期的,你可以在后台记录这个 Id 。一个是 AccessToken 短期的用于 API 请求,用 Freshtoken 去生成 AccessToken ,比如只给 48 小时。用 Refreshtoken 生成 AccessToken 时你可以检查这个 RefreshToken 是否注销了。这样频繁的请求用的是 AccessToken ,有效期短,可自验证。RefreshToken 使用频率低,可严格验证(比如退出后就在后端标记已注销,这样就无法再生成 AccessToken )。

    现在好多 API 对接都是这么设计的。
    3dwelcome
        2
    3dwelcome  
       2022-04-20 18:08:53 +08:00   ❤️ 1
    我用第二种方式,用户在登录时 token 和密码兼容,二选一。

    后台我管这玩意叫 userpin ,然后往里面塞一大堆东西,比如分配用户的数据库 ID ,登录时的 APP ID 号,hash 后的部分密码,预计失效过期时间,客户端识别 ID 之类,防修改签名码。当然全部数据,都加密过再传到前端。

    由于塞进了好多东西,最终的 token 很大,反正用户也看不见。(手动狗头)
    SSang
        3
    SSang  
    OP
       2022-04-20 18:16:25 +08:00
    @Chad0000 确实,OAuth 就是这么设计的,但实际上这个 RefreshToken 的有效期也不会很长,比如默认是 30 天,像 gitlab 、github 的 token 往往有 90 天,180 天,甚至是永久有效期。所以感觉实际设计时候不太能用这种方式。

    然后其实我想讨论的就是这个 Refresh Token 要怎么存,要不要带用户信息,如果不带如何索引等。这个比较纠结,其实如果能想明白这个,我就不纠结用不用 Refresh Token 了
    SSang
        4
    SSang  
    OP
       2022-04-20 18:20:30 +08:00
    @3dwelcome 那这种就是数据库存 token 的 hash 值了吗,比如像 gitlab 还有 personal_access_token 他可以创建好多个,那登录时候,如果有加随机盐,就得一个一个地取盐然后 hash 校验,这个索引效率就很低了。
    3dwelcome
        5
    3dwelcome  
       2022-04-20 18:22:07 +08:00   ❤️ 1
    "像 gitlab 、github 的 token 往往有 90 天,180 天,甚至是永久有效期。"

    你是怕 token 时间太长泄露吗?我个人觉得问题不大,后台都是可以控制的。

    类似 B 站的 cookie, 接近一年前的我都还在用,也没啥问题。
    Chad0000
        6
    Chad0000  
       2022-04-20 18:23:47 +08:00   ❤️ 1
    @SSang 存那么长时间我就不带用户信息了,直接用 GUID+有效期,你可以使用 JWT 让它可以自验证包括有效期,直接用 GUID 也无妨反正后端要验证 RefreshToken 。AccessToken 倒是可以使用 JWT 带上你想要的信息,但如果 API 每次都先去缓存里读一下用户信息的话,那么 JWT 中放不放倒是没太大意义了。

    我是在 JWT 放了最基本信息了比如用户 Id/Email/类型,反正也只是比只存 GUID 大了一点点,我后台可以暂时每次都去验证一下(在缓存中也很快),等后面压力大了可以随时关闭不验证也是可以的。
    SSang
        7
    SSang  
    OP
       2022-04-20 18:39:31 +08:00
    @3dwelcome

    > 你是怕 token 时间太长泄露吗?我个人觉得问题不大,后台都是可以控制的。

    不是,我是觉得 refresh_token + access_token 的使用场景,一般适用于相对短期的认证,比如比较主流的 oauth 、oidc 就是用户先通过用户密码登录,然后服务端返回 refresh_token 和 access_token ,当 access_token 过期时,拿 refresh_token 刷新。和 git 上面的 用户令牌 是由用户申请,之后直接拿 token 访问 api 不太相同。

    > 类似 B 站的 cookie, 接近一年前的我都还在用,也没啥问题。

    这种隐私性不高的页面一般确实长时间持有 token 也问题不大,但是比较敏感的,比如 阿里云的后台,AWS 的后台界面,这种,他们就不会长时间持有 token (没记错的话他们两个的有效期都是 18 小时)
    SSang
        8
    SSang  
    OP
       2022-04-20 18:44:30 +08:00
    @Chad0000

    我比较核心的使用场景是,用于 CI 中调用,或用于命令行脚本调用。这种场景,比如 CI 这种无交互的就不能用户登录,必须提前把 token 配进去,然后他的有效期一般都是以年为单位的,甚至有些低权限的是永久有效期。

    这种场景感觉用 refresh token 就有点奇怪,一是因为我用命令行实际上不好存 access_token ,如果是 CI 里面,存缓存也有点奇怪。二是定时任务往往超过 access_token 的有效期,比如每个月只执行一次的任务,相当于每次我其实都是拿 refresh_token 去请求,和 access_token 就没关系了。
    Chad0000
        9
    Chad0000  
       2022-04-20 19:07:48 +08:00   ❤️ 1
    @SSang 那就不纠结,只给 AccessToken ,JWT 格式,只存 GUID 或用户 ID ,以年为单位。等啥时候想存更多了,将这种存少了的 Token 直接返回需要重新登录就行了。
    SSang
        10
    SSang  
    OP
       2022-04-20 19:32:00 +08:00
    @Chad0000

    嗯,感觉纠结半天还是用回了 JWT 。

    上次有个有个帖子有人说 JWT 存有状态就是 ** 我还给他点了个赞。(笑哭)
    SSang
        11
    SSang  
    OP
       2022-04-20 19:33:45 +08:00
    @SSang 嗯,不如说,是用了 json 格式的 token ,实际上不用来做 JWT 的无状态校验。
    IvanLi127
        12
    IvanLi127  
       2022-04-20 20:19:30 +08:00 via Android   ❤️ 1
    我觉得,就在持久的数据库上存 sessionid + 用户信息完事了。。。出事了全部清掉要求用户重新登录呗。长期的 token 里带用户信息就是单纯地增加风险和长度。。。。
    所以,没特殊架构的话,直接一个 hash 做为 token 就行了。。。
    另外,这个长期 token 可不安全呐,有条件的话,一段时间换一次。。。
    SSang
        13
    SSang  
    OP
       2022-04-20 20:33:06 +08:00
    @IvanLi127 是的,长期 token 一定更不安全,但是确实有使用场景,比如 git 上面我想要每次发版本时候自动获取项目的一些信息,就得调用 api ,如果只有短期 token ,用户很有可能会把账号密码直接存储下来自动获取 token ,这就不是我们想看到的结果了。

    要相对安全点就是得用 api token ,可以申请一个范围很小的,比如只能读项目信息的 token ,相对来说,泄漏后造成的损失也就比较小了。方便和安全之中总要取得某些平衡。
    SSang
        14
    SSang  
    OP
       2022-04-20 20:36:21 +08:00
    说到这里,我就想起来一个比较经典的密码学问题。

    安全的密码要求人在不同系统使用不同的密码,并且要有高复杂度,这本是好事,但由于系统太多,密码太复杂,没法记清楚,于是大多数用于选择将密码写在便利贴上,以方便登录时候能快速获取密码。然后便利贴被盗了。
    nothingistrue
        15
    nothingistrue  
       2022-04-21 10:45:34 +08:00
    你把登录认证跟后续认证当成一会事看才会产生那么多问题,但这俩有区别。在技术上它俩确实是一回事,都是做身份认证的,但是使用场景上不一样。

    登录认证,是用于用户 /客户端接入系统的第一道认证,它面向的是完全未知的用户 /客户端,所以要做高级别的校验——用户名+密码、两部认证、动态令牌,直到最高级别的物理令牌。

    静态令牌认证,即你现在说的这种令牌,这是后续认证。后续认证是不能单独存在的,它必然要依附于登录认证,所以它可以做低级别的验证——比如只要有用户 ID 就放行。因为验证级别很低,服务器甚至都不用保存它,单靠实时算法就可以验证它的真假。(静态令牌认证里面可能会带昵称、日期等杂七杂八的信息,这些是为了使用方便,不是做认证用的)

    比如说卧铺检票,只有上车的时候才会检查车票,这时候它会收走你的车票把它换成卧铺专票——随便打印一下没啥防伪措施的票,后面就只查这个专票,不查车票了。

    有了上面的区别就好解释了,静态令牌因为是后续认证,它只需要一个用户主键就能完整验证,不需要密码等敏感信息,也不需要存储。同时,你不能完全相信静态令牌,不管是短期还是长期的,你都要在服务器端做额外的控制。

    你的方法一,不存在。那就是用户名密码,token 是画蛇添足。你应该再看看 JWT 的用法,JWT 场景下服务器是不存令牌的,它只配置一个全局使用的密钥。JWT 的安全级别也不高,就相当于一个不能伪造的用户主键,当然这是为了自动登录方便的后续认证,这样用没问题。JWT 有不少安全问题,但那本来就不是 JWT 该解决的问题。

    你的方法二,它怎么存储的并不重要,因为这个 token 跟 JWT 一样,就是代表用户主键,服务器从这个 TOKEN 解析出来用户主键之后,再拿用户主键去做后面的授权认证(这里你把认证跟授权也搞成一回事了,这也是两码事)。至于安全性吗,你这里举例得场景是 git push ,这玩意真得没啥安全要求,PR/MR 外加保护主分支,普通分支阿猫阿狗随便 push 都没事,哪怕 push -force 覆盖了以前的内容,git 的分布式外加 Github 的备份措施,也能给恢复过来。说直白点,Github 的用户令牌丢了,最多就是丢脸,没啥卵影响。
    qiany
        16
    qiany  
       2022-04-21 11:46:13 +08:00
    @nothingistrue 静态令牌如果不存储怎么做验证? 额外的控制又是啥?
    nothingistrue
        17
    nothingistrue  
       2022-04-21 14:22:40 +08:00
    @qiany 说个简单的场景,就用户名、密码、JWT 、自动登录这几个元素。
    1.1 、第一次登录,客户端发送用户名、密码;
    1.2 、服务器根据存储的用户名,密码做比对来验证(密码是散列码或者密文保存,这个不在这里讨论);
    1.3 、验证通过后,服务器生成 JWT 令牌,返回给客户端;
    1.4 、客户端保存这个 JWT 令牌;

    到此,登录认证阶段结束。

    2.1 、所有必须需要先登录的接口,在请求时都要带上这个 JWT 令牌;
    2.2 、服务器尝试解码这个 JWT 令牌,如果能解码,就算认证通过,否则返回认证失败;
    2.3 、(可选的授权认证,这玩意后台管理系统用得多,普通系统一般不用) JWT 令牌通常都会包含用户主键,服务器那这个主键,去查找这个用户的授权,跟当前的操作需要的授权,做对比,验证当前用户是否允许这个操作;

    到此,后续认证及授权阶段结束。因为除了登录、注册以及其他不需要登录就能访问的接口,都要做后续认证,这样自动登录就没必要做了,或者说上面的后续认证,就是自动登录。

    上面的过程中,生成和解析 JWT 令牌,需要一个密钥,这个要服务器保存下来。通常(不是超高安全级别要求),整个系统就使用一个密钥,这个密钥直接放配置文件就行,不需要放数据库。服务器不会保存 JWT 令牌,这个客户端自己保存就够了。用户名密码这种验证是一种验证方式,令牌使用的是另外的验证方式,不一样(具体的我也不太清除,有兴趣的话你可以再查查)。


    至于额外的控制,还拿上面的场景说。令牌是明文的,如果只是上面的处理,攻击者只要能够截取到这个令牌(或者攻击客户端读取到这个令牌),它就可以用这个令牌模仿对应用户的访问。所以当认证要求高的时候,就不能只认令牌,需要其他信息来辅助认证。对于敏感操作,应当直接不认这个令牌,要让用户重新登录,然后回到传统的 Session 认证。普通访问,也可以检测当前客户端的 IP 所在地,如果不在用户经常登录所在地(这个信息要保存到缓存)当中,就也不认这个令牌,让客户端重新登录。如果客户端是手机 APP ,还可以在令牌中保存 APP 的唯一识别信息,然后每次认证时都对比令牌中的唯一识别信息跟实际的信息是否相符来做辅助认证(不过这个只防君子不防小人,因为能伪造令牌就能伪造唯一识别信息)。
    SSang
        18
    SSang  
    OP
       2022-06-08 15:08:47 +08:00
    回来看了一下,发现我表述有点乱,导致跑题了,总结几点吧

    1. 还是不要试图用 JWT 来颁发长期令牌了,安全问题 JWT 这种无状态的 token 本来就没法解决。

    2. 长期甚至永久令牌本身就有其使用场景,没必要考虑长期令牌的不安全这个问题,真要说的话长期密码更不安全,但我们不也一直在用吗,作为服务提供商,应该是要考虑如何保证长期令牌的安全。

    3. 本贴讨论的主要是自动化身份认证,所以需要人为介入的都不在讨论范围(比如要登录,要用户输入密码,要用户确认等)

    4. 本帖讨论的为身份认证,上面 nothingistrue 把身份认证拆分成了登录认证和后续认证,不是我想问的问题,参考 API Token (就是一步身份认证,没有再分两步了)

    5. 感觉大家一直在说 JWT ,但其实我一直就不想用 JWT (参考第一点)
    卧铺专票这种就和 JWT 一样,只能用于短期,就不说了,参考第 1 点。

    6. 敏感操作也可以不用讨论了,参考第三点,判断操作敏感度这个属于 AuthZ 了

    7. nothingistrue 说怎么存储不重要,是因为你把这玩意理解成 authz 了,其实他都还没到 authz ,认证合法性就是 authn ,都不用考虑这个 token 有什么权限,怎么存储关系的是 authn 过程的索引效率问题。(然后 github 令牌丢了可不只是丢脸这么简单,不要只考虑 push 的场景)

    8. 然后反驳一下自己的观点:key 用 json 存用户信息和加密 token 没什么意义,效率还低,直接 key 存明文 token ,value 用 json 存用户信息即可(不是 JWT )。加密 token 只能防止数据库泄漏导致 token 明文泄漏,但权限依然会泄漏,token 明文泄漏对别人来说没有意义,和密码不同。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3317 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 13:35 · PVG 21:35 · LAX 06:35 · JFK 09:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.