Skip to content

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。

底层机制 ​

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 (官方推荐)QueuedInterceptorAxios Response Interceptor
凭据安全存储EncryptedSharedPreferences / MMKVflutter_secure_storageHttpOnly Cookie / IndexedDB
并发锁处理synchronized / MutexDio.lock() / QueuedInterceptorPromise 串行排队

常见场景与代码实现 ​

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-simulationJWT 三段式结构 + 双 Token + 并发 401 排队AuthTokenSimulation.java
  • labs/shared/backend-data/auth-token-demo/ — 双 Token 无感刷新 OkHttp Authenticator 实现与并发 401 排队测试

复习检查题 ​

  1. 在 Android OkHttp 中,为什么推荐使用 Authenticator 接口而不是普通的 Interceptor 来处理 Token 刷新?

    答:因为 Authenticator 是 OkHttp 专门为处理 HTTP 401 Unauthorized 鉴权失败设计的响应钩子。只有当服务器明确返回了 401 状态码时才会触发 Authenticator;而普通的 Interceptor 必须在响应后手动判断状态码,且 Authenticator 天生支持在刷新成功后自动返回一个新的 Request 让 OkHttp 自动重新发起原始请求,代码结构更清晰优雅。

  2. 为什么说 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。

站点构建时间:2026/8/24 23:43:17