第 5 周实验指南:令牌颁发与刷新令牌轮换
1. 学习目标
这是重要的一周!我们终于要用一个真正的实现来替换 DummyTokenManager
。我们将构建 OAuth 2.0 服务器的引擎,负责创建、验证和刷新令牌。完成本次实验后,你将能够:
-
实现访问令牌和刷新令牌的创建。
-
理解访问令牌和刷新令牌的不同生命周期。
-
为增强安全性实现刷新令牌轮换(Refresh Token Rotation)。
-
实现刷新令牌盗窃检测。
2. 理论背景
2.1 访问令牌 vs. 刷新令牌
-
访问令牌 (AT):这是客户端应用用来访问受保护资源(例如 API)的令牌。访问令牌的生命周期很短(例如 15 分钟),以限制泄露时造成的损害。它们会随每个 API 请求一起发送。
-
刷新令牌 (RT):这是一种特殊的令牌,用于在旧的访问令牌过期时获取新的访问令牌。刷新令牌的生命周期很长(例如 30 天),但通常只能在令牌端点使用一次。它们由客户端安全存储,不会随每个 API 请求发送。
这种分离在安全性和用户体验之间取得了很好的平衡。用户不必每 15 分钟登录一次,但功能强大的令牌(RT)暴露的频率远低于 AT。
2.2 刷新令牌轮换
当客户端使用刷新令牌获取新的访问令牌时,该刷新令牌应该如何处理?
-
标准方法:服务器返回一个新的访问令牌,同一个刷新令牌可以稍后再次使用。
-
轮换方法:服务器返回一个新的访问令牌和一个新的刷新令牌。旧的刷新令牌立即失效。
轮换更安全。如果刷新令牌被泄露,攻击者可以用它无限期地生成新的访问令牌。通过轮换,攻击者只能使用一次。当合法用户下次使用它时,服务器会发现旧令牌被再次使用,意识到它被盗了,并可以使整个会话失效。
3. 实验任务
导航到 smart-sso-starter-server
模块中的 LocalTokenManager.java
文件。你将在这里实现完整的令牌生命周期。
任务 1:配置 Spring 使用 LocalTokenManager
首先,回到 SmartSsoServerConfiguration.java
文件。注释掉 return new DummyTokenManager();
并启用 LocalTokenManager
的实现。
任务 2:初始化缓存
在 LocalTokenManager
的构造函数中,初始化三个缓存:
-
accessTokenCache
:按访问令牌String
索引存储TokenContent
。它应该有一个较短的超时时间(accessTokenTimeout
)。 -
refreshTokenCache
:按刷新令牌String
索引存储TokenContent
。它应该有一个较长的超时时间(refreshTokenTimeout
)。 -
tgtCache
:这是一个反向查找缓存。它按 TGTString
索引存储一个Set<String>
的刷新令牌。这对于单点登出至关重要:当 TGT 被撤销时,我们可以使用此缓存找到并使所有关联的刷新令牌失效。
任务 3:实现核心方法
-
create(String refreshToken, TokenContent content)
方法:
- 使用 content.getAccessToken()
作为键,将内容放入 accessTokenCache
。
- 使用 refreshToken
作为键,将内容放入 refreshTokenCache
。
- 将 refreshToken
添加到 tgtCache
中与 TGT 关联的令牌集合中。
-
getByAccessToken(String accessToken)
方法:
- 从 accessTokenCache
中检索内容。
-
get(String refreshToken)
方法:
- 从 refreshTokenCache
中检索内容。
-
refresh(String refreshToken)
方法(轮换的核心):
- 第 1 步:获取并移除:原子地从 refreshTokenCache
中获取并移除令牌内容。如果找不到,说明 RT 无效或已被使用。返回 null
。
- TokenContent content = refreshTokenCache.getIfPresent(refreshToken);
- if (content == null) { ... }
- refreshTokenCache.invalidate(refreshToken);
- 第 2 步:盗窃检测:移除旧 RT 后,检查它是否属于因疑似被盗而被标记为撤销的已知令牌家族。如果是,则撤销整个家族并返回 null
。
- 第 3 步:使旧访问令牌失效:使关联的旧访问令牌失效:accessTokenCache.invalidate(content.getAccessToken());
- 第 4 步:生成新令牌:创建一个新的访问令牌和一个新的刷新令牌。
- String newAccessToken = createAccessToken();
- String newRefreshToken = createRefreshToken();
- 第 5 步:更新内容:用新的令牌值更新 content
对象。
- 第 6 步:重建关联:调用你自己的 create(newRefreshToken, content)
方法,将新令牌及其关联存储在缓存中。
- 第 7 步:返回新内容:返回更新后的 content
对象,它现在包含了新的令牌。
4. 测试与验证
- 运行
LocalTokenManagerTest.java
:这是一个复杂的测试套件。它验证:
- 令牌的正确创建和检索。
- 访问令牌和刷新令牌的独立过期。
- 刷新令牌轮换:刷新时会颁发新的 AT 和 RT,并且旧的会失效。
- 盗窃检测:使用已轮换(旧的)刷新令牌会失败,并导致整个令牌家族被撤销。
- 调试与修复:这是迄今为止最具挑战性的实现。在
refresh
操作期间,广泛使用调试器来跟踪你三个缓存的状态。确保你的逻辑在必要时是原子的,以防止竞争条件。