Lex's Blog

Authorization - Session、(JWT) Token

最近想要了解 supabase 的登录相关内容,刚好把之前所了解的知识梳理一下做个记录

Authorization

我们需要一些麻烦的手段(密码、验证码、2FA...)来证明 “我是我”

为了减少麻烦的次数(没人会想跳转一次网页就登录一次),在验证通过后,需要一个 “东西”,它可以代替(密码、验证码、2FA)去证明 “你是你”

且从安全角度,这个 “东西” 应该是不可(极难)伪造的

为了更好沟通,下面就将这个 “东西” 称作 “凭证” 吧

流程

当用户输入账号密码登录成功的时候,由服务端生成凭证并交给客户端,客户端在后续的请求中,将凭证发送给服务端进行身份验证和识别

这里有几件核心的事情:生成凭证 -> 交给客户端 -> 发送给服务端 -> 服务端验证

生成凭证

如果只是为了识别不同的用户,那只要每个用户的凭证是唯一的就可以了

  • 比如:用户的邮箱/手机号、自增的数字、时间戳 + 随机数、UUID 等等

但身份认证还有一个重要的方向是安全:凭证应该是不可(极难)伪造的(不能冒充别人)

显然诸如邮箱、自增数字、时间戳之类的都是无法达到这个目的的

UUID 是可以的,并且为了进一步的安全完全可以在 UUID 的基础上再去添加如邮箱、随机数等加密手段

除此之外的任何安全加密手段,只要能达到:凭证唯一且不可伪造,就可以拿来作为凭证

交给客户端

这里有个隐含的逻辑是客户端拿到后,需要存起来以供后续使用

Important

Cookie 只是一种存储机制,它只是用来存 “凭证” 的,它 不是 “凭证”
Cookie 应该与 Local Storage、IndexDB 进行比较和讨论,它们才是一类的事物

Cookie 是一种浏览器的存储机制,特点是

  • 可以使用 HTTP Response Header 进行设置(由服务端控制)
  • 在浏览器请求匹配域名时,会自动把 Cookie 给添加进 Request Header Cookie

Cookie 的好处是,客户端可以完全不关心这些事情,服务端自己写代码和逻辑就好了

服务端只和浏览器沟通(代码),不需要和客户端沟通

Client

和客户端协商具体的方案,具体细节不重要,可以拿到凭证并存起来就好

比如:登录接口中将凭证作为响应返回,客户端存在 localStorage 中

发送给服务端

如果使用的是 Cookie,那浏览器会搞定

  • Cookie 的场景限制:跨域、CSRF 攻击、非浏览器场景等

如果是其他方案,则按具体方案执行,常见的如将凭证放在 Request Header Authorization 中

服务端验证

服务端验证大体分为两类:存储验证、凭证自验证

存储验证(Session)

存储验证是指,在生成凭证时,将凭证存起来(数据库)

验证时只需要查询下凭证是否存在,是否过期就可以了

优势是

完全可控,如果想实现

  • 强制用户退出,常见于
    • 只允许 N 台设备登录
    • 用户修改密码、密码泄露后防损
  • 限制用户登录 (黑名单、违规操作)

这些只需要操作数据库就可以了

缺点是

  • 数据库压力大,处理速度慢,每个请求、每次验证都需要去查询
    • 可以接入 Redis 缓解,但添加 Redis 同样会增加成本和复杂度

凭证自验证(JWT/Token)

自验证是指,凭证本身就可以验证其有效性,不需要额外的存储

常见的就是签名,使用密钥对数据(用户名、过期时间等)进行加密,将加密结果作为凭证的一部分进行拼接下发

{email:"xxx",expire:"xxx"}-ES256({email:"xxx",expire:"xxx"})

当收到凭证时,只需要分割出数据,并对数据再次签名,就可以知道数据是否被伪造过

只要数据没有被伪造,我们就可以信任数据中的内容(用户名、过期时间等)

TIP

JWT 在此基础上又添加了元信息,用来提供加密算法名称等额外数据

优点是

  • 验证速度快,只需加密操作,不需要外部系统的参与
  • 只要有密钥,任何服务都可以独自验证(分布式)
  • 对于客户端:不需要发送请求就可以拿到用户的基本信息

缺点是

  • 已经发出的凭证,无法主动收回
  • 对于客户端:如果用户信息中的内容变了,看到的依然是旧的
    • 不建议存放会变的数据,只存用户邮箱、ID 等
    • 另一种方式是数据变了之后,重新换一个新的 Token(RefreshToken,见下面)
最佳实践

鉴于 Token 无法收回的特性,往往会伴随 RefreshToken 的配合使用,同时生成 AccessToken 和 RefreshToken 下发给客户端

  • RefreshToken 是存储验证的,会存储在数据库中,所以它不需要用 JWT 之类的方案,只要是不可伪造的唯一值即可

AccessToken 会设置一个较短的过期时间(比如 60min),当过期时可以使用 RefreshToken 来换取新的 AccessToken 和 RefreshToken

  • 每个 RefreshToken 只能使用一次,用完就作废了
  • AccessToken 的过期时间,应该根据服务器的压力(RefreshToken 请求)和安全性进行综合考虑,太长了有安全问题,太短了服务器压力增加(如果短到一定程度,就等同于存储验证了,Token 的自验证优势也就没了,还增加了一堆复杂度)

当我们需要实现诸如:强制退出登录、黑名单等逻辑时,删除数据库中的 RefreshToken 即可,这不是实时的,只有当 AccessToken 过期尝试用 RefreshToken 时才会真正起作用

Supabase

Q: 那 Supabase 用的是什么呢?
A: 全用了

是的,Supabase 用了 JWT,所以会有 AccessToken 和 RefreshToken

同时 Supabase 将 AccessToken 和 RefreshToken 都存储在了数据库中

然后为我们提供了以下方法:

  • getClaims
    • 直接从 Token 中获取用户信息,不发送请求。推荐优先使用此方法,而不是使用 getUser
    • 必须使用 非对称 JWT 签名密钥才起作用,否则还是会调用 getUser 从服务器获取信息
  • getUser
    • 始终会为每个 JWT 向身份验证服务器发送请求

也即是说同时为我们提供了两种方案,可以自行决定使用哪种

对于为什么要使用 JWT 代替 Session,可以查看 Supabase 的 FAQ

关于实时性,Supabase 认为 大多数应用程序很少需要如此严格的安全保证

REF

觉得有帮助?给个 Star 支持一下吧!

Star on GitHub

On this page