CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/Refresh Token
RawBlame
课时目录12 节

OAuth 2.0 与 OpenID Connect

  1. 01OAuth 是什么
  2. 02授权码流
  3. 03PKCE 与 SPA
  4. 04OpenID Connect
  5. 05Scope 与权限
  6. 06Refresh Token
  7. 07Spring OAuth2 Client
  8. 08Resource Server
  9. 09第三方登录
  10. 10OAuth vs 本站 JWT 登录
  11. 11常见安全问题
  12. 12综合练习

Refresh Token

轮换、过期、存储安全。

25 分钟

Refresh Token

这一课你将能做到

  1. 说明「短 access + refresh 续期」的分工与安全收益,写出 grant_type=refresh_token 的完整请求
  2. 按客户端类型选对 refresh 存储位置:后端 DB/Redis、移动端系统钥匙串,绝不长期放前端 localStorage
  3. 实现 rotation(轮换)与重放检测:旧 refresh 再次出现 = 泄露信号,应撤销整条授权

为什么要学

access_token 的寿命是一道两难题:设 7 天,被偷了就是 7 天敞开的大门;设 5 分钟,用户每隔几分钟重新登录,产品没法用。

Refresh token 是标准答案,分工明确:

  • access_token:日常开门用,寿命短(几分钟到几十分钟),泄露窗口小;
  • refresh_token:只干一件事——去授权服务器换新 access_token,寿命长(天级)但用途单一,不能直接调业务 API。

这套思想你也用在本站的自研 JWT 方案里:backend 签发短寿命 JWT,配合 refresh(或滑动续期)保证体验——第 10 课会专门对比两条轨。而对接第三方 IdP 或自建授权服务器时,refresh 的协议细节(请求形状、轮换、撤销)必须按标准来实现,不能「长寿 access 一发了之」。

面试高频:「为什么 access token 要短」「refresh token 泄露了怎么办」「rotation 是什么」——本课全部给到可背诵、可落地的程度。

先建立图像

登录成功(授权码流换到 token)
  ├─ access_token   15 分钟 ──► 调业务 API(常过期,泄露损失小)
  └─ refresh_token  7~30 天 ─► 仅用于 POST /token 换新 access
                                    │
                        开启 rotation 时:响应同时返回新 refresh
                                    │
                        旧 refresh 立即作废
                                    │
                        若作废的旧 refresh 又被拿来用
                                    └─► 重放!撤销该用户该 Client 的全部授权

按客户端类型选存储位置:

客户端refresh 放哪原因
机密后端服务端 DB/Redis(加密存储)可绑定用户与客户端,易撤销
SPA不要长期放 localStorageXSS 一锅端;优先放 BFF/后端
移动 AppiOS Keychain / Android Keystore系统级加密保护

跟着做

步骤 1:看清续期请求的形状

# 中文注释:用 refresh 换新 access;按 IdP 要求带 client_id/secret 或客户端标识
curl -i -X POST https://auth.example.com/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=refresh_token" \
  -d "refresh_token=THE_REFRESH_TOKEN" \
  -d "client_id=coderoadmap-web" \
  -d "client_secret=${CLIENT_SECRET}"

期望响应包含新的 access_token、expires_in;若开启 rotation,还有新的 refresh_token——此时旧的立即作废,代码里必须更新存储。

步骤 2:前端感知过期的标准流程(BFF 思路)

浏览器只持本站短会话(HttpOnly cookie)
  → 调 API 收到 401
  → 前端调 /auth/refresh(BFF 接口)
  → BFF 用库存 refresh 调 IdP /token
  → 成功:发新本站 JWT,前端重试原请求
  → refresh 也失效:清会话,引导重新走登录

关键点:IdP 的 refresh 永远留在服务端,浏览器只见本站会话。这与本站自研 JWT 的刷新接口是同一个设计模式。

步骤 3:实现 rotation 与重放检测(Java 伪码可落地)

// 中文注释:refresh 用 Redis 存储;key 带 jti;轮换 = 删旧发新
public String[] rotateRefresh(String userId, String presentedJti) {
    String stored = redis.get("refresh:" + userId + ":" + presentedJti);
    if (stored == null) {
        // 旧票已作废却再次出现 → 疑似泄露:撤销该用户全部授权
        revokeAllGrants(userId);
        throw new SecurityException("refresh 重放,已撤销全部授权,请重新登录");
    }
    redis.delete("refresh:" + userId + ":" + presentedJti);   // 轮换:旧票即焚
    String newAccess  = jwtUtil.sign(userId, loadRoles(userId)); // 15 分钟
    String newRefresh = randomToken();                            // 7 天
    redis.setex("refresh:" + userId + ":" + jti(newRefresh),
                Duration.ofDays(7).toSeconds(), userId);
    return new String[]{newAccess, newRefresh};
}

步骤 4:对照本站自研方案的刷新接口

# 中文注释:本站 JWT 登录的续期走自家接口,思想与 OAuth refresh 相同
curl -i -X POST http://localhost:8080/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "本地Redis中登记的refresh串"}'

对照表:

OAuth refresh:与 IdP 之间的协议续期,跨应用边界,grant 可被用户在 IdP 侧撤销
本站自研 JWT:backend 自己签发自己验,Redis 登记可撤销,逻辑同源但不需要协议
共同纪律:短 access + 可撤销 refresh + 日志打码

步骤 5:登出与撤销清单

用户点登出 / 改密 / 解绑 GitHub:
  ① 删除本站会话(cookie / 本站 JWT 失效标记)
  ② 调 IdP 的 revocation 端点(若支持)作废 refresh
  ③ 清掉服务端库存 refresh(Redis 删 key)
  ④ 审计日志只记 jti 或前后缀打码,绝不记全文

RFC 7009 定义了标准撤销端点:

# 中文注释:向 IdP 主动撤销 refresh;第三方「解绑授权」同理
curl -i -X POST https://auth.example.com/revoke \
  -d "token=THE_REFRESH_TOKEN" \
  -d "client_id=coderoadmap-web" \
  -d "client_secret=${CLIENT_SECRET}"

步骤 6:设计一组时长并写进笔记

access  = 15 分钟(短,泄露窗口小)
refresh = 7 天 + rotation(体验与安全的平衡点)
7 天无任何续期动作 → refresh 自然过期 → 重新走授权码登录

讲清楚要点

  1. access 的泄露面等于它的寿命,所以生产环境宜短 access + refresh 续期,而不是发一枚超长 access 图省事。
  2. refresh 的能力等于「无限续票」,存储与传输要求高于任何业务字段:加密存储、仅走 HTTPS、不进日志、不进前端。
  3. SPA 的 refresh 应留在 BFF/后端:浏览器只持短会话 cookie;XSS 偷不到不存在于页面的东西。
  4. rotation 把「泄露检测」变成了协议能力:旧票重放 = 强信号,触发全量撤销。不开启轮换时,至少要支持主动撤销。
  5. 登出、改密、解绑必须走撤销链路:只删浏览器 cookie 而不清服务端 refresh,等于「门锁换了钥匙串还在小偷手里」。
  6. 本站密码登录已有自己的会话续期逻辑,不要硬塞 OAuth refresh 概念;联邦轨才与 IdP 的 refresh 打交道——两条轨各用各的续期机制,互不混淆。

你可能会懵的地方

  • 没有 refresh 能不能做 OAuth? 能。refresh 是「可选」的:access 过期后重新走一次授权码即可。很多 IdP 对公共客户端默认不发 refresh,需要按文档开启。

  • refresh token 是 JWT 吗? 通常是不透明随机串(安全性依赖服务端存储比对),不要假设能本地解码、不要本地判断过期——有效性以授权服务器的回答为准。

  • 自动刷新会不会和并发请求打架? 会。页面同时发 5 个请求都 401,就可能触发 5 次刷新;配合 rotation 时,第 2 次开始全用旧票 → 误判重放 → 全量撤销。解法:同一用户加锁/单飞(single-flight),刷新期间其余请求排队等待新 token。

  • Client Credentials 有 refresh 吗? 通常没有——机器身份直接再申请一枚新 access 即可,没有终端用户参与,不适用「用户委托」的续期模型。

  • refresh 有效期设多长合适? 看产品:金融类 1 天甚至更短;一般 Web 应用 7-30 天。配合 rotation 与「长期不活跃即过期」策略,比单纯拉长更安全。

错法对照

错法问题正解
access 设 30 天「省得刷新」泄露窗口 30 天短 access(15 分钟级)+ refresh
refresh 存 localStorageXSS 一偷一个准服务端 / 系统钥匙串
日志打印完整 refresh日志泄露=授权泄露打码或只记 jti/hash
rotation 后仍接受旧 refresh重放检测失效,偷票者畅通旧票重放 → 撤销全部授权
前端 401 就清会话弹登录体验差且浪费合法会话先走 refresh 续期,失败再登录

卡点急救

  1. 现象:续期请求报 invalid_grant。按概率排查:refresh 已被 rotation 作废(还在用旧票);refresh 过期;client_id/secret 不匹配;grant 已被用户在 IdP 侧撤销。前三者重新登录即可,第四种要引导用户重新授权。
  2. 现象:开启 rotation 后用户被随机「全量登出」。典型原因是并发刷新打架:多个请求同时拿同一枚旧 refresh 换新,后者被判重放。上单飞刷新:全局同一时刻只放行一个刷新请求,其余等待复用结果。
  3. 现象:前端收到 401 直接弹登录,从不尝试续期。拦截器逻辑写反了。顺序:401 → 调 refresh 接口 → 成功则替换 token 并重放原请求 → 失败才清会话跳登录。注意「重放原请求」只对幂等或用户可接受的重试场景做。

面试常问

  • 为什么要短 access + 长 refresh,而不是一枚长 access? access 是每次请求都外发的凭证,暴露面大,必须短命控制泄露窗口;refresh 只在续期时出现一次,暴露面小、用途单一(换新 access),可以长一些。两者配合兼顾安全与体验。

  • refresh token 泄露了怎么办? 短期:立即撤销该 refresh 与其派生的全部 access(撤销端点/删 Redis);根因修复:确认日志、存储、传输环节;长期:启用 rotation——偷票者与正常用户必有一方先用到已作废的旧票,重放检测会让全量授权被撤销,攻击即暴露。

  • rotation 的原理和代价? 每次续期发放新 refresh 并立即作废旧票;旧票再次出现即视为泄露信号,撤销整条授权。代价是需要处理并发刷新(单飞)与「合法旧票在途请求」的竞态,实现上要小心但值得。

自测三问

  1. 写出 refresh 换票的 grant_type 与至少两个必带参数。
  2. 为什么 SPA 不能长期存 refresh?推荐架构是什么?
  3. rotation 下「旧 refresh 再次出现」应如何处置?为什么?

小结一句话

Refresh = 短 access 的安全续命索:短 access 控暴露面,refresh 专职续期;存服务端、开轮换、可撤销、日志打码。

延伸

  • oauth.net:Refresh Tokens
  • RFC 7009:Token Revocation
  • 下一课:Spring OAuth2 Client——把前几课的协议步骤全部交给 Spring Security,只写「登录成功之后」的钩子。
加载本节练习…
← 上一课Scope 与权限下一课 →Spring OAuth2 Client