构建用户管理微服务(六):添加持久 JWT 令牌的 remember me 身份验证

《构建用户管理微服务》系列文章已经连载五期,即将接近尾声。本期的「译见」将带你用持久令牌添加经典的 remember-me 模式认证,先演示一下运行的用户管理应用程序是如何工作的, 然后再深入细节。

译见

在上期「译见」系列文章《构建用户管理微服务(五):使用 JWT 令牌和 Spring Security 来实现身份验证》中,使用 Spring Security 添加了基于用户名和密码的身份验证。但需要注意的是,JWT 令牌是在成功登录后发出的,并验证后续请求。创造长时间的 JWT 是不实际的,因为它们是相互独立的,且没有办法撤销它们。如果令牌被盗,其后果也无法挽回。因此,我想用持久令牌添加经典的 remember-me 模式认证。Remember-me 令牌存储在 Cookie 中 JWTs 作为第一道防线, 但是它们也被保存到数据库中, 并且追踪它们的生命周期。

这次我想先演示一下运行的用户管理应用程序是如何工作的, 然后再深入细节。

有关 Token | 船长导语

诸如 Facebook,Github,Twitter 等大型网站都在使用基于 Token 的身份验证。相比传统的身份验证方法,Token 的扩展性更强,也更安全,非常适合用在 Web 应用或者移动应用上。我们将 Token 翻译成令牌,也就意味着,你能依靠这个令牌去通过一些关卡,来实现验证。

验证流程

基本上, 在用户使用用户名/密码进行验证时所发生的情况, 致使他们表示希望应用程序记住他们的意图 (持久会话)。大多数情况下, 用户界面上会有一个附加的复选框来实现。但由于应用程序还没有开发一个用户界面, 我们便使用 cURL 来实现这一切。

登录

成功认证后,PersistentJwtTokenBasedRememberMeServices 会创建一个持久会话,将其保存到数据库并将其转换为 JWT 令牌。它负责将此持久会话存储在客户端的一个 cookie(Set-Cookie)上,并且还发送新创建的临时令牌。后者旨在在单页前端的使用寿命内使用,并使用非标准 HTTP 头(X-Set-Authorization-Bearer)进行发送。

当 rememberMe 标记为错误时,只创建一个无状态的 JWT 令牌,并且完全绕过 remember-me 基础架构。

在应用程序运行时只使用临时令牌

当应用程序在浏览器中打开时,它会在每个 XHR 请求的授权头文件中发送临时 JWT 令牌。然而,当应用程序重新加载时,临时令牌将丢失。

为了简单起见,这里使用 GET / users / {id}来演示正常的请求。

持久令牌与临时令牌一起进行使用

当用户在第一种情况下选择 remember-me 认证时,会发生这种情况。

在这种情况下,临时 JWT 令牌和一个有效的 remember-me cookie 都是同时发送的。只要单页应用程序正在运行,就使用临时令牌。

使用持久令牌进行初始化

当前端在浏览器中加载时, 不知道是否存在任何临时 JWT 令牌。所能做的就是通过尝试执行一个正常的请求来测试持久的记住我的 cookie。

如果持久令牌 (cookie) 仍然有效, 它将在数据库中进行更新, 使其在上次使用时保持的记录在浏览器中也会得到更新。另外一个重要的步骤也是执行, 用户无需给他们的用户名/密码和一个新的临时令牌就会自动获得身份验证。从现在开始, 只要它在运行, 应用程序就会使用临时令牌。

注销

虽然注销操作看起来似乎很简单, 但还是有一些需要我们注意的细节。只要用户已通过身份验证, 前端就仍然无法发送无状态的 JWT 令牌,否则用户界面上的注销按钮就不会被提供, 后端也不知道如何注销。

在此请求之后, remember-me cookie 将被重置, 并且数据库中的持久会话也被标记为 “已删除”。

实现 Remember-me 身份验证

正如我在摘要中提到的,我们将使用持久令牌来增加安全性,以便能够在任何时候进行撤销。我们需要执行三个步骤,以确保用 Spring Security 处理 remember-me。

实现 UserDetailsService

在第一篇文章中,我决定使用 DDD 开发模型,因此它不能依赖于任何框架特定的类。实际上,它甚至不依赖于任何第三方框架或库。大多数教程通常直接实现 UserDetailsService,并且业务逻辑和用于构建应用程序的框架之间没有额外的层。

UserServices 在该项目的第二部分就已经被添加,因此我们的任务非常简单,因为现在我们需要的是一个框架特定的组件,它将 UserDetailsService 的任务委托给现有的逻辑。

它只是围绕 UserService 的简单包装, 最终将返回的 User 模型对象转换为框架特定的 UserDetails 实例。此外, 在这个项目中, 我们不直接使用用户的登录名 (电子邮件地址或屏幕名称)。相反, 他们的用户 id 在各处被传递。

实现 PersistentTokenRepository

幸运的是,我们在添加 PersistentTokenRepository 实现方面同样容易,因为领域模型已经包含SessionService 和 Session 。

它的情况与 UserDetailsService 大致相同,包装器会在 PersistentRememberMeToken 和Session 之间进行转换 。唯一需要特别注意的是 PersistentRememberMeToken 中的日期字段。在会话中,我分离了两个日期字段(ie. issuedAt 和 lastUsedAt),当用户在 remember-me 令牌的帮助下首次登录时, 后者获取其第一个值。因此有可能它是空的,而这时,issuedAt 的值将会作为替代。

实现 RememberMeServices

在这一点上, 我们重新使用 PersistentTokenBasedRememberMeServices,并为手头的任务进行自定义, 这取决于 UserDetailsService 和 PersistentTokenRepository 已然被实现。

此特定实现使用 JWT 令牌作为实例化窗体, 用于在 cookie 中存储 remember-me 令牌。Spring Security 的默认格式已经很好了,但 JWT 增加了一个额外的安全层。用于检查 remember-me 令牌,默认实现没有签名,每个请求最终都是数据库中的一个查询。

JWT 防止了这种情况的发生,尽管解析它并验证其签名需要更多的 CPU 周期。

将他们组合在一起

其效果在最后部分是显而易见的。基本上,这是关于使用 Spring Security 注册组件,并启用 remember-me 服务的全部过程。AbstractRememberMeServices 也是此设置中的默认注销处理程序,并在注销时将数据库中的令牌标记为已删除。

Gotchas

在 POST 请求正文中接收用户凭据和 remember-me 标记为 Json 数据

默认情况下, UsernamePasswordAuthenticationFilter 会将凭据作为 POST 请求的 HTTP 请求参数,相对的,我们希望发送的是 JSON 文档。进一步的,AbstractRememberMeServices 还会检查是否存在 remember-me 标志作为请求参数。为了解决这个问题,LoginFilter 将 remember-me 标志设置为请求属性,并将 PersistentTokenBasedRememberMeServices 的决定委派给 remember-me 的身份验证是否需要启动。

使用 RememberMeServices 处理登录成功

RememberMeAuthenticationFilter 不会继续进入过滤器链中的下一个过滤器,但如果设置了AuthenticationSuccessHandler 它将停止其执行 。

登录

ProceedingRememberMeAuthenticationFilter 是原始过滤器的自定义版本,当认证成功时,该过滤器不会停止。

下期预告:构建用户管理微服务(七):合而为一

原文链接:https://www.springuni.com/user-management-microservice-part-6

往期回顾

译见|构建用户管理微服务(一):定义领域模型和 REST API

译见|构建用户管理微服务(二):实现领域模型

译见|构建用户管理微服务(三):实现和测试存储库

译见|构建用户管理微服务(四):实现 REST 控制器

译见|构建用户管理微服务(五):使用 JWT 令牌和 Spring Security 来实现身份验证

Leave a Reply

Your email address will not be published. Required fields are marked *