Access Token / Refresh Token 认证实现流程
从登录、短期 Access Token、长期 Refresh Token、Server-side Session/Grant 到刷新、并发控制和失败收口,说明 Hybrid Token 认证的完整实现流程。
[!info] related notes
- 所属 MOC: Node.js 后端面试 MOC, 后端开发 MOC
- 前置概念: Access Token, Refresh Token, Server-side Session 鉴权, JWT
- 架构关系: Stateful、Stateless 与 Hybrid 认证架构
- 浏览器边界: Cookie、Session、Token 与 JWT 的关系, HttpOnly Cookie, CSRF 跨站请求伪造
- 凭证变更: Credential Change & Session Revocation
- Account Recovery: Password Reset & Account Recovery
- Session Trust: Active Sessions & Device Management, Reauthentication Freshness
- Risk/Events: Risk-Based Authentication, Authentication Security Events & Login History
Access Token / Refresh Token 认证实现流程
目标
这篇描述一种典型 Hybrid 认证链路:
长期登录生命周期
→ Server-side Session / Grant
普通 API 请求
→ short-lived Access Token
Access Token 续期
→ Refresh Token
它不是所有 Web 应用的默认答案。普通 Browser Web App 如果直接用 opaque Session Cookie 更简单,就没有必要为了“使用 JWT”额外增加刷新协议。
总体架构
┌─────────────────────┐
│ Session / Grant Store│
│ user / revoke / TTL │
└──────────┬──────────┘
│
Login / Refresh ────────────┤
│ issue
▼
short-lived Access Token
│
▼
Resource Server
local verify fast path
核心分工:
Session / Grant
→ 长期生命周期 Source of Truth
Access Token
→ 高频短期访问凭证
Refresh Token
→ 低频续期凭证,进入长期状态控制面
1. Login
客户端提交登录凭证:
email / username + password
服务端:
parse request
→ lookup user
→ verify password hash
→ check account state
→ create Session / Grant
→ create/bind Refresh Token
→ issue short-lived Access Token
→ return credentials / set cookie
顺序上,长期可撤销状态应该先成功建立,再把依赖它的凭证交给客户端;否则可能出现“凭证已发出,但服务器无法按设计控制其生命周期”的中间状态。
2. 普通 API 请求
客户端携带 Access Token:
Authorization: Bearer <access-token>
如果它是短期 JWT,Resource Server 可以:
verify signature
→ check exp / nbf
→ check iss / aud
→ read sub / scope
→ establish principal
→ run resource authorization
在典型 Hybrid fast path 中,这一步不需要每次查询 Session Store。
因此普通请求性能路径可以保持接近 Stateless。
3. 资源级 Authorization
Access Token 只能提供身份和粗粒度 claims,不能替代业务资源检查。
例如:
sub = user 42
scope = assessment:write
仍然不代表 User 42 可以修改任何 Assessment。
业务层还需要判断:
WHERE assessment.id = :id
AND assessment.user_id = :authenticated_user_id
或使用更复杂的 Policy / membership 规则。
4. Access Token 过期
当 API 因 Access Token 过期返回认证失败,客户端进入刷新流程。
original request
→ Access Token expired
→ refresh once
→ get new Access Token
→ retry original request
Refresh Endpoint 不应该依赖旧 Access Token 才能工作;它自己的认证凭证就是 Refresh Token。
5. Refresh
Browser 场景常见:
Refresh Token
→ HttpOnly + Secure Cookie
→ browser auto-sends to /auth/refresh
服务端:
read Refresh Token
→ verify / hash lookup
→ find Session / Grant
→ check revoked / expired
→ check user state
→ rotate Refresh Token if used
→ issue new Access Token
这里才回到 Stateful 生命周期控制面。
6. Refresh Token Rotation
如果使用 rotation:
RT1
↓ refresh
AT2 + RT2
RT1 becomes invalid
若 RT1 之后再次出现:
possible copied credential / replay
→ revoke refresh family or Session
关键不是“生成新 Token”本身,而是:
consume old credential
+
create replacement
必须尽量形成原子 single-winner transition。
7. 并发 401 / Refresh Storm
一个页面往往同时有多个 API 请求。Access Token 恰好过期时,不能让每个 401 都独立刷新。
更合理:
request A gets 401
→ starts refresh
request B/C/D get 401
→ wait same refresh promise
refresh succeeds
→ all retry with new Access Token
客户端应实现 single-flight / refresh lock,而不是并发打多个 refresh 请求。
8. 如何避免 Refresh 请求递归触发刷新
旧实现常通过 X-Skip-Auth 自定义 Header 标记特殊请求,但这只是其中一种客户端实现技巧,并不是认证协议的一部分。
更干净的做法通常是二选一:
独立 Auth Client
apiClient
→ 自动注入 Access Token
→ 自动处理 401
authClient
→ login / refresh
→ 不经过普通 401 refresh interceptor
请求级内部配置
在 HTTP Client 自己的 config metadata 中标记:
skipAccessTokenInjection = true
skipAutoRefresh = true
这个标记只存在客户端代码内部,不需要真的发送给服务器,也就不需要增加 CORS Header。
核心原则只有一个:
刷新请求不能再次进入“Access Token 过期 → 自动刷新”的同一递归路径。
9. 浏览器存储策略
不要把“Access + Refresh 都放 localStorage”当默认答案。
一种常见边界:
Access Token
→ short-lived
→ memory
Refresh Token
→ long-lived
→ HttpOnly + Secure Cookie
页面 reload 后 Access Token 消失,可以通过 refresh/bootstrap 恢复。
如果系统使用 BFF 或纯 Server-side Session,则浏览器甚至不需要直接接触 Access Token。
10. Logout
Hybrid 架构中,Logout 不应该只清前端变量:
client clears access state
+
server revokes current Session / Refresh family
+
clear Refresh Cookie
“退出所有设备”则撤销用户所有 Session/Grant。
注意:如果已经签发的 Access Token 走完全本地验证,它可能继续有效到自身 exp。需要即时撤销时,可以:
- 对敏感操作额外查实时状态;
- 缩短 Access Token TTL;
- 引入 denylist/introspection;
- 或直接采用每请求 Session 验证。
11. Refresh 失败如何收口
Refresh 失败后不要无限循环:
refresh failed
→ stop retries
→ clear local authenticated state
→ clear invalid credential if appropriate
→ redirect / ask user to authenticate again
要区分网络暂时失败和明确的 invalid_grant / revoked / expired,避免把所有瞬时错误都当成永久登出。
12. Access Token TTL 怎么定
没有“必须 5 分钟”这一固定标准。
TTL 是以下因素的权衡:
shorter TTL
→ smaller stale/replay window
→ more refresh traffic
longer TTL
→ fewer refreshes
→ revoked/changed state may stay stale longer
根据系统风险、客户端环境、Refresh 可靠性和服务规模选择。
13. 是否一定要用 JWT
不一定。
Access Token
→ JWT or opaque
Refresh Token
→ JWT or opaque
JWT 的主要价值是在多 Resource Server 场景中允许本地验证自包含 claims。
如果每次请求本来就要查同一个 Session Store,那么直接使用 opaque Session Token 可能更简单。
最稳的架构表述
Hybrid 认证把长期登录生命周期放在服务端 Session/Grant 中,用短生命周期 Access Token 服务高频 API 请求;Access Token 过期后,客户端用 Refresh Token 回到 Auth Server,服务器检查 Session/Grant、撤销和用户状态后再签发新的短期 Access Token。普通请求可以无状态验证,Refresh/Logout/Revocation 则回到有状态控制面。
最短流程
Login
→ create Session / Grant
→ issue Access + Refresh
API
→ Access Token local verify
→ business authorization
Expired
→ Refresh Token
→ check server state
→ rotate / issue new Access
Logout
→ revoke Session / Refresh family
无提示执行检查
不看上文,画出并解释完整链路:
- Login 成功后,Server-side Session/Grant、Access Token、Refresh Token 分别承担什么职责?
- 普通 API 请求为什么只验证 Access Token 还不够,仍要做资源级 Authorization?
- Access Token 过期后,Refresh 请求为什么需要重新检查服务端状态?
- Refresh Token Rotation 要解决什么攻击窗口?为什么要处理 token family / reuse?
- 多个请求同时 401 时,如何避免 refresh storm?
- 为什么 Refresh 请求不能被普通 401 interceptor 递归触发自己?
- 设计一条 Logout 后旧 Access Token 仍有短暂 stale window 的收口策略。