跳到主要内容

记住我认证

QWen Max 中英对照 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
txt

记住我令牌仅在指定的期限内有效,并且仅当用户名、密码和密钥未更改时才有效。值得注意的是,这存在一个潜在的安全问题,即捕获的记住我令牌可以从任何用户代理使用,直到该令牌过期。这与摘要认证存在的问题相同。如果主体意识到令牌已被捕获,他们可以轻松更改密码并立即使所有已发布的记住我令牌失效。如果需要更高级别的安全性,你应该使用下一节中描述的方法。或者,根本不应使用记住我服务。

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

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

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

持久化令牌方法

这种方法基于文章 改进的持久登录 Cookie 最佳实践,并进行了一些小的修改 [1]。要使用带有命名空间配置的方法,你需要提供一个数据源引用:

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

数据库应包含一个 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)
ddl

记住我接口和实现

记住我功能与 UsernamePasswordAuthenticationFilter 一起使用,并通过 AbstractAuthenticationProcessingFilter 超类中的钩子实现。它也在 BasicAuthenticationFilter 中使用。这些钩子在适当的时候调用一个具体的 RememberMeServices。以下代码展示了该接口:

public interface RememberMeServices {

void loginFail(HttpServletRequest request, HttpServletResponse response);

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

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void logout(HttpServletRequest request, HttpServletResponse response);
}
java
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

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

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

我们之前已经了解到 Spring Security 提供了两种实现。我们依次来看一下每种实现。

TokenBasedRememberMeServices

此实现支持简单哈希令牌方法中描述的更简单的方法。TokenBasedRememberMeServices 生成一个 RememberMeAuthenticationToken,该令牌由 RememberMeAuthenticationProvider 处理。一个 key 在此身份验证提供者和 TokenBasedRememberMeServices 之间共享。此外,TokenBasedRememberMeServices 需要一个 UserDetailsService,从中可以检索用户名和密码以进行签名比较,并生成 RememberMeAuthenticationToken 以包含正确的 GrantedAuthority 实例。TokenBasedRememberMeServices 还实现了 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;
}
java

在应用程序上下文中,需要以下 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;
}
java

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

PersistentTokenBasedRememberMeServices

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

  • InMemoryTokenRepositoryImpl 仅用于测试。

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

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


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