跳到主要内容
版本:7.0.2

记住我认证

DeepSeek V3 中英对照 Remember Me Remember-Me Authentication

记住我(Remember-me)或持久登录认证指的是网站能够在不同会话之间记住用户的身份。这通常通过向浏览器发送一个cookie来实现,该cookie在未来的会话中被检测到,从而触发自动登录。Spring Security为这些操作提供了必要的钩子,并提供了两种具体的记住我实现。一种使用哈希来确保基于cookie令牌的安全性,另一种则使用数据库或其他持久化存储机制来存储生成的令牌。

请注意,两种实现都需要一个 UserDetailsService。如果您使用的身份验证提供程序不使用 UserDetailsService(例如 LDAP 提供程序),则除非您的应用程序上下文中同时存在一个 UserDetailsService bean,否则它将无法工作。

基于哈希令牌的简易方法

该方法利用哈希技术实现了一种实用的“记住我”策略。本质上,在交互式身份验证成功后,会向浏览器发送一个cookie,该cookie的构成如下:

base64(username + ":" + expirationTime + ":" + algorithmName + ":"
algorithmHex(username + ":" + expirationTime + ":" password + ":" + key))

username: As identifiable to the UserDetailsService
password: That matches the one in the retrieved UserDetails
expirationTime: The date and time when the remember-me token expires, expressed in milliseconds
key: A private key to prevent modification of the remember-me token
algorithmName: The algorithm used to generate and to verify the remember-me token signature

记住我令牌仅在指定期限内有效,且仅在用户名、密码和密钥未更改时有效。值得注意的是,这存在潜在的安全问题:一旦记住我令牌被截获,在令牌过期前,任何用户代理都可以使用它。这与摘要认证存在的问题相同。如果用户意识到令牌已被截获,可以轻松更改密码,从而立即使所有已签发的记住我令牌失效。如果需要更高的安全性,则应采用下一节描述的方法。或者,根本不应使用记住我服务。

如果你熟悉命名空间配置章节中讨论的主题,可以通过添加 <remember-me> 元素来启用记住我认证功能:

<http>
...
<remember-me key="myAppKey"/>
</http>

UserDetailsService 通常会被自动选择。如果您的应用上下文中存在多个实例,则需要通过 user-service-ref 属性指定应使用哪一个,该属性的值应为您的 UserDetailsService bean 的名称。

持久令牌方法

该方法基于文章 改进的持久登录 Cookie 最佳实践 并做了一些细微修改 [1]。要在命名空间配置中使用此方法,您需要提供一个数据源引用:

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

数据库应包含一个 persistent_logins 表,可通过以下 SQL(或等效语句)创建:

create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)

记住我接口与实现

Remember-me 功能与 UsernamePasswordAuthenticationFilter 结合使用,并通过其超类 AbstractAuthenticationProcessingFilter 中的钩子实现。它同样在 BasicAuthenticationFilter 中被使用。这些钩子会在适当的时机调用具体的 RememberMeServices 实现。以下代码展示了该接口:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication);

请参阅 RememberMeServices 的 Javadoc 以获取关于这些方法功能的更完整讨论,但请注意,在此阶段,AbstractAuthenticationProcessingFilter 仅调用 loginFail()loginSuccess() 方法。每当 SecurityContextHolder 不包含 Authentication 时,RememberMeAuthenticationFilter 就会调用 autoLogin() 方法。因此,该接口为底层的 remember-me 实现提供了关于认证相关事件的充分通知,并在候选 Web 请求可能包含 cookie 并希望被记住时委托给实现。这种设计允许使用任意数量的 remember-me 实现策略。

我们之前已经了解到,Spring Security 提供了两种实现。接下来我们将逐一探讨这两种实现。

TokenBasedRememberMeServices

此实现支持基于哈希令牌的简单方法中描述的简化方案。TokenBasedRememberMeServices 会生成一个 RememberMeAuthenticationToken,该令牌由 RememberMeAuthenticationProvider 进行处理。此认证提供者与 TokenBasedRememberMeServices 之间共享一个 key。此外,TokenBasedRememberMeServices 需要一个 UserDetailsService,以便从中检索用户名和密码用于签名比对,并生成包含正确 GrantedAuthority 实例的 RememberMeAuthenticationTokenTokenBasedRememberMeServices 还实现了 Spring Security 的 LogoutHandler 接口,因此可与 LogoutFilter 配合使用,实现自动清除 cookie 的功能。

默认情况下,此实现使用 SHA-256 算法对令牌签名进行编码。为了验证令牌签名,系统会解析并使用从 algorithmName 中检索到的算法。如果 algorithmName 不存在,则将使用默认的匹配算法,即 SHA-256。您可以为签名编码和签名匹配指定不同的算法,这样用户可以在没有 algorithmName 的情况下安全地升级到不同的编码算法,同时仍能验证旧的签名。为此,您可以将自定义的 TokenBasedRememberMeServices 指定为 Bean,并在配置中使用它。

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.rememberMe((remember) -> remember
.rememberMeServices(rememberMeServices)
);
return http.build();
}

@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256;
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm);
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5);
return rememberMe;
}

在应用上下文中启用记住我服务需要以下bean:

@Bean
RememberMeAuthenticationFilter rememberMeFilter() {
RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter();
rememberMeFilter.setRememberMeServices(rememberMeServices());
rememberMeFilter.setAuthenticationManager(theAuthenticationManager);
return rememberMeFilter;
}

@Bean
TokenBasedRememberMeServices rememberMeServices() {
TokenBasedRememberMeServices rememberMeServices = new TokenBasedRememberMeServices();
rememberMeServices.setUserDetailsService(myUserDetailsService);
rememberMeServices.setKey("springRocks");
return rememberMeServices;
}

@Bean
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
RememberMeAuthenticationProvider rememberMeAuthenticationProvider = new RememberMeAuthenticationProvider();
rememberMeAuthenticationProvider.setKey("springRocks");
return rememberMeAuthenticationProvider;
}

请记得将你的 RememberMeServices 实现添加到 UsernamePasswordAuthenticationFilter.setRememberMeServices() 属性中,在 AuthenticationManager.setProviders() 列表里包含 RememberMeAuthenticationProvider,并将 RememberMeAuthenticationFilter 添加到你的 FilterChainProxy 中(通常紧接在 UsernamePasswordAuthenticationFilter 之后)。

PersistentTokenBasedRememberMeServices

你可以像使用 TokenBasedRememberMeServices 一样使用这个类,但它还需要配置一个 PersistentTokenRepository 来存储令牌。

  • InMemoryTokenRepositoryImpl 仅用于测试目的。

  • JdbcTokenRepositoryImpl 将令牌存储在数据库中。

请参阅持久令牌方法了解数据库模式。


1. 本质上,用户名不包含在 cookie 中,以防止不必要地暴露有效的登录名。本文的评论部分对此进行了讨论。