Token 是什么?
Token 是一个凭据,拿着这个凭据能证明用户(可能是人,也可能是第三方系统)在系统内的身份。
常见的 Token 类型有 Basic(用户名、口令)、Bearer(OAuth2 令牌)、Digest(用户名、口令摘要),以及大模型 API 常用的 API_KEY、开放平台常用的 Access/Secret Key。
一些 Token 是不太安全的,其中以 Basic 尤甚。所以通常的做法是用 Basic 访问认证接口后,换取其他类型的 Token,如 Bearer。
有状态 Token v.s. 无状态 Token
有状态/无状态是针对服务端而言的。如果 Token 中自包含了用户的身份信息(用户名、邮箱等),那么它是无状态的 Token;反之,需要服务端根据 Token 获取对应的用户,则是有状态的 Token。
有状态 Token
有状态 Token 的最常见的解决方案是借助 Session 机制,即用 Session ID 作为 Token,将用户信息放入 Session 中。或者也可以将用户 ID 放入 Session,每次去数据库中查询用户信息。后者需要考虑性能问题,但是用户信息更新后无需更新 Session。
值得说明的是——Session 与 有状态 Token 并非等价或包含关系,而仅仅是有交集。
例如 Session 在未认证(没有 Token)的情况下,也可以存储诸如购物车的信息。
Token 也可以不存储在 Session 中,比如内存中维护一个 Map,或者存入 Redis。
无状态 Token
无状态 Token 的事实标准是 JWT。JWT 是将用户的身份信息(JSON 格式)进行 Base64 编码,然后生成摘要并进行签名。利用未掌握密钥的攻击者无法伪造签名的特性,JWT 可以自证不是被伪造的。
JWT 的签名有对称和非对称两类。如果是单体架构的应用,那么可以选择对称加密,它的优点是加密快。 如果是微服务架构,则必须使用非对称加密。其中私钥只能由认证服务持有,其他服务持有公钥,或者从认证服务对外暴露的 JWKS 接口获取公钥。
JWT 的缺点是不能被废弃,比如一个用户被禁用了,但是其仍然能凭借已经签发的 Token 进行操作,即重放攻击。对此需要额外的处理,通常的方案是使用双 Token(即 access_token + refresh_token) + 短期 Token 黑名单。其中 access_token 是无状态的,但是有效期很短(通常在 5 分钟内),每当 access_token 过期后,就使用 refresh_token 重新从后端获取新的 access_token。要废弃一个 Token,则将双 token 放入黑名单,对所有请求都查询一遍是否存在于黑名单。
Cookie v.s. Web Storage
Token 需要前端进行存储,并在调用接口时携带 Token。
以前的方案是服务端设置 Set-Cookie 响应头,浏览器会将 Token 写入 Cookie。后续的请求,浏览器也会自动带上 Cookie。
另一种方案(为了兼容移动端)则是拿到 Token 后,前端编写代码,将其存入 sessionStorage/localStorage(存入 Cookie 理论上也是可行的,但是实在没有必要,也不安全)。请求时由请求头携带
由此可以看出 Cookie/WebStorage 与 有状态/无状态 并无关联。Cookie 也可以处理 JWT,WebStorage 也能处理 Session ID。
顺带一提,使用 Cookie 的方案要注意 CSRF 攻击,Web Storage 方案要注意 XSS 攻击。
选择建议
对于单体应用,建议选择有状态的 Token。
对于微服务应用,可以让认证服务签发有状态的 Token(refresh_token)。每次请求时,由 API 网关将持 refresh_token 向认证服务兑换一个极短期(1分钟左右)的 JWT 格式的 access_token,并将其放入请求头。