第 3 周实验指南:全局会话管理 (TGT)

第 3 周实验指南:全局会话管理 (TGT) 1. 学习目标 本周,我们将实现单点登录的核心:由票据授予票据(TGT)代表的全局会话。完成本次实验后,你将能够: 理解 TGT 在 SSO 系统中的作用。 实现 TGT 的创建、检索和移除。 使用带有生存时间(TTL)的缓存实现会话过期。 为会话实现滑动窗口过期策略。 限制每个用户的并发会话数量。 2. 理论背景 2.1 什么是票据授予票据 (TGT)? TGT 是进入 SSO 王国的主钥匙。用户首次登录后,SSO 服务器会创建一个 TGT 并存储它,将其与用户身份关联。然后,服务器在用户的浏览器中设置一个包含 TGT ID 的 Cookie。 当用户访问另一个应用时,他们的浏览器会将此 TGT Cookie 发送到 SSO 服务器。服务器验证 TGT 以确认用户拥有活动会话,如果验证通过,则为新应用颁发访问令牌,而无需再次请求用户登录。这就是单点登录的精髓。 2.2 会话管理策略 生存时间 (TTL):会话不应永久有效。我们定义一个超时时间(例如 30 分钟),非活动会话在此之后自动过期。 滑动窗口过期:为了改善用户体验,我们可以在用户活动时刷新会话的过期时间。如果用户执行操作,他们 30 分钟的会话计时器将重置。这可以防止他们在仍在使用应用时被登出。 并发会话限制:出于安全和资源管理的考虑,我们可能希望限制一个用户可以同时登录的设备数量(例如,最多 5 个活动会话)。 3. 实验任务 导航到 smart-sso-starter-server 模块中的 LocalTicketGrantingTicketManager.java 文件。你将在这里使用缓存实现管理 TGT 的逻辑。 任务 1:初始化缓存 在构造函数中,你需要使用 Guava 的 CacheBuilder 初始化两个缓存: ...

September 2, 2025 · 1 min · Theme PaperMod

第 4 周实验指南:授权码管理

第 4 周实验指南:授权码管理 1. 学习目标 本周,我们专注于 OAuth 2.0 的基石——授权码流程。我们将实现创建和验证短期有效的授权码的逻辑,并添加一项至关重要的安全功能:代码交换证明密钥(PKCE)。完成本次实验后,你将能够: 理解授权码在 OAuth 2.0 流程中的作用。 实现授权码的创建和一次性使用。 理解授权码被截获的威胁。 2. 理论背景 2.1 授权码流程 该流程被认为是 Web 和移动应用最安全的流程,分两步进行: 授权请求:用户从客户端应用重定向到 SSO 服务器的授权端点。用户登录后(或如果他们已有 TGT 会话),服务器会生成一个临时的、一次性的授权码,并通过预先注册的重定向 URI 将其发回给客户端。 令牌请求:然后,客户端应用的后端安全地将此授权码(连同其客户端密钥)发送到 SSO 服务器的令牌端点。服务器验证该码,如果有效,则返回最终的访问令牌和刷新令牌。 这个两步过程是安全的,因为宝贵的访问令牌从未暴露在用户的浏览器(前端通道)中。 2.2 PKCE (代码交换证明密钥) 如果用户移动设备上的恶意应用在授权码返回给合法应用时截获了它怎么办?攻击者随后可以用它来交换访问令牌。PKCE(读作“pixy”)可以防止这种情况。 客户端创建密钥:在开始流程之前,客户端应用会生成一个名为 code_verifier 的随机字符串。 客户端创建挑战:然后,它对此验证器进行哈希处理(通常使用 SHA-256)以创建一个 code_challenge。 带挑战的授权请求:客户端在初始授权请求中将 code_challenge 和哈希方法(S256)发送到 SSO 服务器。服务器将这些与授权码一起存储。 带验证器的令牌请求:当客户端用授权码交换令牌时,它也包含了原始的、明文的 code_verifier。 服务器验证:服务器使用存储的方法对提供的 code_verifier 进行哈希,并检查它是否与存储的 code_challenge 匹配。如果匹配,服务器就知道请求来自原始客户端。只截获了授权码的攻击者将不会有原始的验证器。 3. 实验任务 导航到 smart-sso-starter-server 模块中的 LocalCodeManager.java 文件。你将在这里实现管理授权码的逻辑。 任务 1:初始化缓存 在构造函数中,初始化一个名为 codeCache 的 Guava Cache。 键将是授权码 String。 ...

September 2, 2025 · 1 min · Theme PaperMod

第 5 周实验指南:令牌颁发与刷新令牌轮换

第 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 的构造函数中,初始化三个缓存: ...

September 2, 2025 · 2 min · Theme PaperMod

第 6 周实验指南:令牌撤销与单点登出

第 6 周实验指南:令牌撤销与单点登出 1. 学习目标 在我们的最后一次实验中,我们将实现令牌撤销和单点登出(SSO)这两个至关重要的安全功能。完成本次实验后,你将能够: 理解明确撤销令牌的重要性。 实现一个用于令牌撤销的端点(遵循 RFC 7009)。 理解后端通道、异步单点登出的概念。 实现当用户会话终止时通知客户端应用的逻辑。 2. 理论背景 2.1 令牌撤销 当用户从一个应用明确登出时会发生什么?客户端应用应该销毁其本地会话,并通知 SSO 服务器使令牌失效。这可以防止即使用户已登出,泄露的刷新令牌仍被使用。RFC 7009 为此定义了一个标准端点。客户端发送其 client_id、client_secret 和要撤销的 token。 2.2 单点登出 (SSO) 单点登录很棒,但它有一个对应物:单点登出。如果用户从一个应用登出,他们是否应该自动从在该 SSO 会话期间访问的所有其他应用中登出?为了安全,是的。 前端通道登出:涉及复杂的浏览器重定向。它不可靠,因为如果其中一个应用宕机,它可能会失败。 后端通道登出:SSO 服务器直接与每个客户端应用的后端通信,通知它们用户的会话已结束。这更可靠。服务器向每个客户端预先注册的登出 URI 发送一个特殊的“登出令牌”。然后,客户端验证此令牌并在其端终止相应的用户会话。 3. 实验任务 我们将向 LocalTokenManager.java 和主要的 LogoutController 中添加最后的逻辑。 任务 1:在 LocalTokenManager 中实现令牌移除逻辑 remove(String refreshToken) 方法: - 此方法将由撤销端点调用。 - 使用 refreshToken 从 refreshTokenCache 中检索 TokenContent。 - 如果找到,则使刷新令牌(refreshTokenCache.invalidate(refreshToken))和关联的访问令牌(accessTokenCache.invalidate(content.getAccessToken()))都失效。 - 同时,从反向查找的 tgtCache 中移除刷新令牌,以清理 TGT 到 RT 的映射。 removeByTgt(String tgt) 方法: - 这是单点登出的核心。当 TGT 过期或被手动移除时,它由我们 TGT 缓存上的 RemovalListener(来自第 3 周)调用。 - 使用 tgtCache 获取与此 TGT 关联的所有刷新令牌的 Set<String>。 - 遍历此集合,并为每个刷新令牌调用你自己的 remove(refreshToken) 方法。这将产生级联效应,使与主 TGT 会话关联的所有令牌都失效。 ...

September 2, 2025 · 1 min · Theme PaperMod

第2周实验指南:用户和客户端应用程序

第 2 周实验指南:用户及客户端应用验证 1. 学习目标 本周,我们将深入 OAuth 2.0 流程的第一个关键步骤:验证用户凭证和客户端应用的身份。完成本次实验后,你将能够: 实现用户认证逻辑,包括密码验证。 增加安全增强功能:多次登录失败后锁定账户。 实现密码过期策略。 实现客户端应用验证,包括客户端密钥和重定向 URI 的检查。 2. 理论背景 2.1 用户认证 在 SSO 服务器授予任何应用访问权限之前,它必须首先验证用户的身份。这通常通过核对数据库中的用户名和密码来完成。为安全起见,密码绝不能以明文形式存储。我们应该使用像 BCrypt 这样强大的单向算法来存储密码的哈希值。当用户尝试登录时,我们对他们提供的密码进行哈希计算,并与存储的哈希值进行比较。 2.2 客户端认证与重定向 URI 在 OAuth 2.0 中,代表用户请求访问的应用被称为客户端。服务器也必须验证这个客户端,通常通过检查其 client_id 和 client_secret 来完成。 一项至关重要的安全措施是验证重定向 URI。用户成功登录后,服务器会带着一个授权码将他们送回客户端应用。服务器必须确保只重定向到预先注册的、可信的 URI,以防止攻击者截获授权码。 3. 实验任务 任务 1:实现 UserServiceImpl 导航到 smart-sso-starter-server 模块中的 UserServiceImpl.java 文件。你会在这里找到 TODO 注释作为指引。 validate(String account, String password) 方法: - 使用 userMapper.selectByUsername(account) 从数据库中获取 User。 - 检查 1:用户存在性:如果用户为 null,返回 Result.error("用户不存在")。 - 检查 2:账户锁定:实现一个缓存(例如 Guava 的 Cache)来跟踪每个用户的登录失败次数。如果一个用户连续登录失败 5 次,将其账户锁定 10 分钟。在检查密码之前,先检查缓存。如果已锁定,返回 Result.error("账号已被锁定...")。 - 检查 3:密码验证:使用注入的 passwordEncoder.matches(password, user.getPassword()) 来比较提供的密码和存储的哈希值。如果不匹配,在你的锁定缓存中记录这次失败,并返回 Result.error("密码错误")。 - 检查 4:用户启用状态:如果 user.getIsEnable() 为 false,返回 Result.error("用户已被禁用")。 - 检查 5:密码过期:检查 user.getPasswordLastModified() 是否已超过 90 天。如果是,返回 Result.error("密码已过期...")。 - 成功:如果所有检查都通过,清除该用户的登录失败缓存,并返回 Result.success(user)。 getTokenUser(Long userId) 方法: - 通过用户 ID 获取 User。 - 创建一个 TokenUser 对象,这是一个简化的用户表示,可以安全地包含在令牌中。 - 关键:不要在 TokenUser 对象中包含密码或其他敏感信息。 任务 2:实现 AppServiceImpl 导航到 AppServiceImpl.java 文件。你也会在这里找到 TODO 注释。 ...

September 2, 2025 · 1 min · Theme PaperMod