Appearance
Token 鉴权体系、JWT、OAuth2 与移动端双 Token 自动无感刷新
回到总览:后台 / API / 数据层
相关模块:API 幂等性、超时重试与移动端数据最终一致性 · H5 登录态与 Cookie 强注入 SSO 机制
一句话定义
Token 鉴权体系是移动端安全通信的核心,通过 AccessToken(短生命周期访问令牌)与 RefreshToken(长生命周期刷新令牌)的双 Token 机制,结合 JWT(JSON Web Token)或 OAuth2 协议,实现用户在无需频繁重新输入密码的前提下的安全请求鉴权与无感无缝续期。
代码索引
对应 Lab:auth-token-simulation
labs/shared/backend-data/auth-token-demo/— 双 Token 无感刷新 OkHttp Authenticator 实现与并发 401 排队测试
为什么需要
- 为什么移动端不能只发一个有效期为 30 天的长效
AccessToken?- 一句话答:如果 AccessToken 有效期长达 30 天,一旦被中间人攻击或恶意软件截获,攻击者在 30 天内均可任意伪造用户身份,且服务端无法主动作废该 Token;双 Token 机制将 AccessToken 设为 2 小时,兼顾了安全性与无感体验。
- 为什么 10 年 Android 工程师做双 Token 无感刷新时必须处理“并发 401”与死锁问题?
- 一句话答:当页面同时并发发起 5 个 API 请求,若 AccessToken 刚好过期,这 5 个请求会同时收到
401 Unauthorized;如果 5 个请求都独立发网络去请求刷新 Token,会导致反复刷新并使前一次的 RefreshToken 作废崩溃;必须使用锁(如 Mutex / Synchronized)确保同一时间仅一次刷新,其余请求排队复用新 Token。
- 一句话答:当页面同时并发发起 5 个 API 请求,若 AccessToken 刚好过期,这 5 个请求会同时收到
底层机制
1. 双 Token 无感刷新标准时序 (Dual-Token Refresh Flow)
text
[移动客户端] ─── (请求 API: Header 带 AccessToken = exp_2h) ───► [后端 API 网关]
│ │
◄─── (返回 401 Unauthorized: AccessToken 已过期!) ─────────────────┘
│
[触发 Authenticator 独占锁]
│
├─── (向 Auth 服务发 POST /refreshtoken, 带 RefreshToken = exp_30d) ───► [Auth 鉴权中心]
│ │
◄─── (返回新 AccessToken 和新 RefreshToken) ────────────────────────────────┘
│
[更新本地保存的双 Token,并用新 AccessToken 自动重试那 5 个被卡住的原始请求!]2. JWT (JSON Web Token) 结构拆解
JWT 是一种无状态的跨域鉴权 Token 格式,由三部分用 . 连接组成:
Header.Payload.Signature
text
1. Header: {"alg": "HS256", "typ": "JWT"} ──► Base64Url 编码
2. Payload: {"sub": "10086", "role": "admin", "exp": 1688001234} ──► Base64Url 编码 (不要存敏感密码!)
3. Signature: HMACSHA256(Header + "." + Payload, secretKey) ──► 密匙签名 (防篡改!)Android / Flutter / Web / Backend 对照
| 环节 | Android (OkHttp) | Flutter (Dio) | Web (Axios) |
|---|---|---|---|
| 无感刷新钩子 | Authenticator (官方推荐) | QueuedInterceptor | Axios Response Interceptor |
| 凭据安全存储 | EncryptedSharedPreferences / MMKV | flutter_secure_storage | HttpOnly Cookie / IndexedDB |
| 并发锁处理 | synchronized / Mutex | Dio.lock() / QueuedInterceptor | Promise 串行排队 |
常见场景与代码实现
1. OkHttp Authenticator 实现并发安全的双 Token 无感刷新
对应 Lab:auth-token-simulation
java
public class TokenAuthenticator implements Authenticator {
private final TokenRepository tokenRepo;
@Override
public Request authenticate(Route route, Response response) throws IOException {
// 1. 防止死循环:如果刷新 Token 的请求本身也返回了 401,放弃重试跳转登录页
if (response.request().url().pathSegments().contains("refreshToken")) {
return null;
}
synchronized (this) {
String currentToken = tokenRepo.getAccessToken();
String requestToken = response.request().header("Authorization");
// 2. 检查:如果已经被同期的另一个并发请求刷新过了,直接用新 Token 重试
if (requestToken != null && !requestToken.endsWith(currentToken)) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + currentToken)
.build();
}
// 3. 发起同步请求刷新 Token
String newToken = tokenRepo.blockingRefreshToken();
if (newToken != null) {
return response.request().newBuilder()
.header("Authorization", "Bearer " + newToken)
.build();
} else {
// 4. RefreshToken 也过期了,触发强制退登
EventBus.post(new LogoutEvent());
return null;
}
}
}
}2. OAuth2 授权码模式(Authorization Code):第三方登录的标准时序
移动端"微信/Google 一键登录"走的就是授权码模式——App 永远不接触第三方账号密码,只交换授权码换取令牌:
图注补充:Auth: 第三方授权服务器 (微信/Google);Svr: 自有后端 (持有 client_secret);Auth: 1. 拉起授权页(带 client_id + redirect_uri + scope);App: 2. 用户同意后回调 redirect_uri 携带 authorization_code;Svr: 3. 把 code 传给自有后端(不走客户端换 token!);Auth: 4. 后端用 code + client_secret 换取 access_token;Svr: 5. 返回 access_token + 用户信息;App: 6. 签发自有业务 Token, 完成登录。
- 观察重点:
client_secret只能保存在服务端——App 若自己拿 code 换 token,就必须内置 secret,等于把第三方账号体系的安全边界暴露给反编译;所以第 3 步一定是"code 交给后端,由后端完成交换"。
常见误配、事故后果与排障
1. 事故:JWT 的 Payload 中放入了用户明文密码或身份证号
- 误配原因:误以为 JWT 整体是加密的,将敏感信息塞入 Payload。
- 后果:JWT 的 Header 和 Payload 仅仅是 Base64 编码,任何人截获该 Token 后均可以轻松解码读取 Payload 里的内容,造成严重 PII 隐私泄漏!
- 排障与修法:Payload 中只存放非敏感标识(如
userId、exp、role);校验合法性全靠第三部分Signature签名。
与相近概念对比
| 鉴权方案 | 是否服务端存 Session | 跨域支持 | 适合场景 |
|---|---|---|---|
| Session + Cookie | 是 (存 Redis/内存) | 较差 (受 SameSite 限制) | 传统 Web 单体应用 |
| JWT (无状态 Token) | 否 (只校验签名) | 极佳 | 微服务架构、移动端 App、跨域 API |
| OAuth2 | 视实现而定 | 极佳 | 第三方授权登录(如微信/Google 登录) |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| auth-token-simulation | JWT 三段式结构 + 双 Token + 并发 401 排队 | AuthTokenSimulation.java |
labs/shared/backend-data/auth-token-demo/— 双 Token 无感刷新 OkHttp Authenticator 实现与并发 401 排队测试
复习检查题
在 Android OkHttp 中,为什么推荐使用
Authenticator接口而不是普通的Interceptor来处理 Token 刷新?答:因为
Authenticator是 OkHttp 专门为处理 HTTP401 Unauthorized鉴权失败设计的响应钩子。只有当服务器明确返回了 401 状态码时才会触发Authenticator;而普通的Interceptor必须在响应后手动判断状态码,且Authenticator天生支持在刷新成功后自动返回一个新的Request让 OkHttp 自动重新发起原始请求,代码结构更清晰优雅。为什么说 JWT (JSON Web Token) 是“防篡改”的,但不是“防读取”的?
答:因为 JWT 的 Header 和 Payload 仅仅是经过了 Base64Url 编码,任何拿到 Token 的人都可以对其进行解码并读取里面的 JSON 内容,因此它不是防读取的;但第三部分
Signature是由服务端使用私钥对 Header 和 Payload 进行哈希生成的,如果有人修改了 Payload 里的数据,服务端校验签名就会失败,因此它是防篡改的。
速记
- 双 Token 分工:AccessToken 存活短 (2h),RefreshToken 存活长 (30d) 专门用于无感换票。
- 无感刷新加锁:并发 401 必须用
synchronized加锁,同一时间仅发一次刷新请求。 - JWT 安全红线:Payload 仅仅是 Base64 编码,严禁放入明文密码与敏感 PII。