鉴权系统设计 - 基于 HttpOnlyCookie, Access Token, Refresh Token...

三葉Leaves Author

基于 HttpOnlyCookie + Access Token + Refresh Token 实现的鉴权和认证模型,在现代应用中很常见。本文将简略总结一下这个系统。

概念

HttpOnlyCookie

可以理解为一个特殊的容器,由于 JS 无法读到 Cookie,所以在这里面存东西能显著降低 XSS 窃取 token 的风险。

跨站脚本攻击(XSS)

全称 Cross-Site Scripting,简写成 XSS 是为了和 CSS 做区分。
这个攻击常见于前端直接把用户的输入(比如评论)不做处理,而放进 HTML 里,导致意料之外的执行了攻击者的脚本。
攻击目标很可能是存在 localstorage 里的 token 之类的信息。所以,我们不把长期 token 存在 localstorage 里,而是存在 HttpOnlyCookie 来阻断这个风险。

跨站请求伪造攻击(CSRF)

全称 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)

不透明 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
2
device_private_key  
device_public_key

然后客户端请求 token:

1
2
POST /token  
public_key = device_public_key

授权服务器签发 token:

1
2
3
4
access_token {  
user: 123
device_key: fingerprint(public_key)
}

在 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
2
sign(data)
decrypt(data)

如此一来,即便 Access Token 失窃了,由于缺少设备密钥,攻击者在短期内也无法完成攻击。

  • 标题: 鉴权系统设计 - 基于 HttpOnlyCookie, Access Token, Refresh Token...
  • 作者: 三葉Leaves
  • 创建于 : 2026-03-06 00:00:00
  • 更新于 : 2026-03-16 12:05:05
  • 链接: https://blog.oksanye.com/0f6c29b480c8/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
鉴权系统设计 - 基于 HttpOnlyCookie, Access Token, Refresh Token...