鉴权系统设计 - 基于 HttpOnlyCookie, Access Token, Refresh Token...
基于 HttpOnlyCookie + Access Token + Refresh Token 实现的鉴权和认证模型,在现代应用中很常见。本文将简略总结一下这个系统。
概念
HttpOnlyCookie
可以理解为一个特殊的容器,由于 JS 无法读到 Cookie,所以在这里面存东西能显著降低 XSS 窃取 token 的风险。
全称 Cross-Site Scripting,简写成 XSS 是为了和 CSS 做区分。
这个攻击常见于前端直接把用户的输入(比如评论)不做处理,而放进 HTML 里,导致意料之外的执行了攻击者的脚本。
攻击目标很可能是存在 localstorage 里的 token 之类的信息。所以,我们不把长期 token 存在 localstorage 里,而是存在 HttpOnlyCookie 来阻断这个风险。
全称 Cross-Site Request Forgery。攻击者不能直接拿到你的身份凭证,但可以借你的浏览器自动带上登录态,替你向目标网站发请求。
例如,在攻击者的网站 evil.com 里,有一个链接指向 POST bank.com/transfer 并附带了向攻击者转账的请求体,这时候你点击链接,浏览器就会拿你的 cookie 去找 bank.com 鉴权,请求最终就能发出去。
解决方案是给 cookie 加 SameSite=Strict ,这样从第三方站点跳过来时就不会带上 cookie 了。
在用户注册或者登录后,后端给登录接口返回的信息里可以带上 set-cookie 来设置浏览器的 cookie (通常往 cookie 里存 refresh token, 下文会讲到)。之后,前端就可以凭借这个 cookie 去找后端换真正可以用于鉴权的 Access Token 了。
Access Token
一个短生命周期的访问凭证。通常在需要鉴权的接口请求时放在请求头(Header)里,作为 Bear Token:
1 | Bear xxxxxxx |
Access Token 通常是 JWT,有时候也会是不透明 token(opaque token)
就是一串看起来随机的字符串,本身读不出业务信息。资源服务器要么拿它去查库,要么调授权服务器的 introspection 接口换取 token 对应的信息。
Access Token 的生命周期要短,比如 5 分钟、15 分钟、30 分钟,以此减少风险窗口。带着 Access Token 调用接口,后端依赖这个完成接口的认证和鉴权。
Access Token 通常存在内存里,亦或者 localstorage 里(更危险)。由于其生命周期很短,所以即便失窃了风险窗口也很小。
Refresh Token
一个较长生命周期的刷新凭证,通常是随机生成的字符串。它的作用只有一个,就是在 Access Token 实效后,用于找服务端换新的 Access Token。
由于 Refresh Token 生命周期更长,所以暴露以后的风险和危险性更高。因此,通常我们把它放在 [[#HttpOnlyCookie]] 里保管。
实际案例
在我做过的一个系统里,我设置了下面这几个接口:
- POST
/user/login:
请求体是账号和密码,登录成功后在响应体里返回 Access Token,并用 Set-Cookie 响应头单独下发 refresh_token Cookie。
- POST
/user/refresh:
没有请求体,请求头里带着 Bearer Token( 也就是 Access Token)。响应体是新的 AccessToken 和令牌类型。刷新成功后轮换并重新设置新的 refresh_token Cookie。
- POST
/user/logout:
用于把 refresh_token 的 Cookie 过期掉。
未来方向
Token 一旦被拿到,攻击者还是可以为所欲为,尤其是 Refresh Token 风险更大。
后来,安全社区开始尝试一个新思路:
token 不应该只证明“谁”,还要证明“谁在用”。
最典型的实现就是:device-bound token。
device-bound token 的核心思想是:
token 只能被某个设备使用,不是任何人拿到都能用。
实现方式通常是:
token + device private key
一个典型流程
登录时:
client 生成一对密钥:
1 | device_private_key |
然后客户端请求 token:
1 | POST /token |
授权服务器签发 token:
1 | access_token { |
在 OAuth 里有两个重要实现:
mTLS tokens 和 DPoP(Demonstration of Proof of Possession)
和 Http Only Cookie 一样,JS 拿不到 device private key ,因为它存在 WebCrypto API 里面。
JS 可以这样用:
1 | crypto.subtle.sign(...) |
但却不可以这样:
1 | crypto.subtle.exportKey(...) |
至于浏览器会把这个 Key 放哪,取决于不同浏览器的实现。
Chrome 系的很可能放在系统的密钥库里,比如 Apple 设备的钥匙串。
Safari 甚至会直接塞进 Secure Enclave,这是苹果自己的安全芯片,CPU 都没法直接读取它,外部只能这样请求:
1 | sign(data) |
如此一来,即便 Access Token 失窃了,由于缺少设备密钥,攻击者在短期内也无法完成攻击。
- 标题: 鉴权系统设计 - 基于 HttpOnlyCookie, Access Token, Refresh Token...
- 作者: 三葉Leaves
- 创建于 : 2026-03-06 00:00:00
- 更新于 : 2026-03-09 15:27:20
- 链接: https://blog.oksanye.com/0f6c29b480c8/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。