Remember-Me Authentication
Remember-me 或持久登录身份验证指的是网站能够在会话之间记住主体的标识。这通常是通过向浏览器发送 Cookie 来完成的,Cookie 将在以后的会话中被检测到,并导致自动登录的发生。Spring Security 为这些操作提供了必要的钩子,并且有两个具体的 remember-me 实现。一种方法使用哈希来保护基于 cookie 的令牌的安全性,另一种方法使用数据库或其他持久存储机制来存储生成的令牌。
请注意,这两种实现都需要一个UserDetailsService。如果您使用的身份验证提供者不使用UserDetailsService(例如,LDAP提供者),那么除非您的应用程序上下文中也包含一个UserDetailsService bean,否则它将无法工作。
Simple Hash-Based Token Approach
这种方法使用哈希来实现一个有用的"记住我"策略。本质上,在成功的交互式身份验证后,一个cookie会被发送到浏览器,该cookie的构成如下:
txt
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
"记住我"令牌仅在指定的有效期内有效,且仅当用户名、密码和密钥没有更改时才有效。值得注意的是,这存在一个潜在的安全问题,即捕获的"记住我"令牌可以在令牌过期之前的任何用户代理上使用。这与摘要认证的问题相同。如果主体意识到令牌已被捕获,他们可以轻松更改密码,并立即使所有"记住我"令牌无效。如果需要更高的安全性,您应该使用下一节中描述的方法。或者,根本不应使用"记住我"服务。
如果您熟悉本章中讨论的命名空间配置主题,您可以通过添加 元素来启用"记住我"身份验证:
java
<http>
...
<remember-me key="myAppKey"/>
</http>
UserDetailsService 通常会自动选择。如果您在应用程序上下文中有多个 UserDetailsService,则需要使用 user-service-ref 属性指定应该使用哪一个,其中的值是您的 UserDetailsService bean 的名称。
Persistent Token Approach
这种方法基于文章《改进的持久登录Cookie最佳实践》 [1] 并进行了一些小的修改。要使用命名空间配置这种方法,您需要提供一个数据源引用:
java
<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>
数据库应该包含一个持久化 _ login 表,该表是通过使用以下 SQL (或等效 SQL)创建的:
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 Interfaces and Implementations
"记住我"功能与 UsernamePasswordAuthenticationFilter 一起使用,并通过 AbstractAuthenticationProcessingFilter 超类中的钩子实现。它也在 BasicAuthenticationFilter 中使用。这些钩子在适当的时候调用一个具体的 RememberMeServices。以下列表显示了该接口:
java
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() 方法。autoLogin() 方法由 RememberMeAuthenticationFilter 在 SecurityContextHolder 不包含 Authentication 时调用。因此,这个接口为底层"记住我"实现提供了足够的认证相关事件通知,并且每当一个候选的 web 请求可能包含一个 cookie 并希望被记住时,就会委托给实现。这种设计允许任意数量的"记住我"实现策略。
我们之前已经看到,Spring Security 提供了两种实现。我们逐一查看这些实现。
TokenBasedRememberMeServices
这个实现支持之前描述的简单基于哈希的令牌方法。TokenBasedRememberMeServices 生成一个 RememberMeAuthenticationToken,该令牌由 RememberMeAuthenticationProvider 处理。这个身份验证提供者和 TokenBasedRememberMeServices 之间共享一个密钥。此外,TokenBasedRememberMeServices 需要 一个 UserDetailsService,从中它可以检索用户名和密码以用于签名比较目的,并生成包含正确 GrantedAuthority 实例的 RememberMeAuthenticationToken。TokenBasedRememberMeServices 还实现了 Spring Security 的 LogoutHandler 接口,因此它可以与 LogoutFilter 一起使用,以自动清除 cookie。
默认情况下,这个实现使用 SHA-256 算法来编码令牌签名。为了验证令牌签名,将从 algorithmName 检索到的算法进行解析并使用。如果没有提供 algorithmName,将使用默认的匹配算法,即 SHA-256。您可以分别为签名编码和签名匹配指定不同的算法,这允许用户在仍然能够验证没有 algorithmName 的旧令牌的情况下,安全地升级到不同的编码算法。为此,您可以指定自定义的 TokenBasedRememberMeServices 作为 Bean,并在配置中使用它。
java
@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 来启用 remember-me 服务:
java
@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() 属性中,将 RememberMeAuthenticationProvider 包含在您的 AuthenticationManager.setProviders() 列表中,并在您的 FilterChainProxy 中添加 RememberMeAuthenticationFilter(通常紧随您的 UsernamePasswordAuthenticationFilter 之后)。
PersistentTokenBasedRememberMeServices
您可以按照与 TokenBasedRememberMeServices 相同的方式使用该类,但是它还需要配置一个 PersisentTokenRepository 来存储令牌。
InMemoryTokenRepositoryImpl
which is intended for testing only.JdbcTokenRepositoryImpl
which stores the tokens in a database.
有关数据库架构,请参见持久性令牌方法。