此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

记住我认证

“记住我”或持久化登录认证是指网站能够在多个会话之间记住主体(用户)的身份。 这通常是通过向浏览器发送一个 Cookie 来实现的,在后续会话中检测到该 Cookie 后,系统会自动执行登录操作。 Spring Security 提供了执行这些操作所需的必要钩子,并提供了两种具体的“记住我”实现方式。 一种使用哈希算法来保障基于 Cookie 的Tokens的安全性,另一种则使用数据库或其他持久化存储机制来保存生成的Tokens。spring-doc.cadn.net.cn

请注意,这两种实现都需要一个 UserDetailsService。 如果你使用了一个不依赖 UserDetailsService 的认证提供者(例如 LDAP 提供者),那么除非你的应用上下文中还包含一个 UserDetailsService bean,否则它将无法正常工作。spring-doc.cadn.net.cn

基于简单哈希的Tokens方法

此方法使用哈希算法来实现一种有效的“记住我”策略。 本质上,在用户成功完成交互式身份验证后,会向浏览器发送一个 Cookie,该 Cookie 的组成如下:spring-doc.cadn.net.cn

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)Tokens仅在指定的时间段内有效,且前提是用户名、密码和密钥未发生更改。 值得注意的是,这存在潜在的安全问题:一旦记住我Tokens被截获,攻击者可在该Tokens过期前从任意用户代理(user agent)使用该Tokens。 这与摘要认证(digest authentication)面临的问题相同。 如果用户意识到自己的Tokens已被截获,可以立即更改密码,从而立刻使所有已签发的记住我Tokens失效。 如果需要更高的安全性,应采用下一节中描述的方法。 或者,完全不要使用记住我服务。spring-doc.cadn.net.cn

如果你熟悉命名空间配置一章中讨论的主题,可以通过添加<remember-me>元素来启用“记住我”身份验证:spring-doc.cadn.net.cn

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

UserDetailsService 通常会自动选择。 如果你的应用上下文中存在多个 user-service-ref,则需要使用 UserDetailsService 属性来指定应使用哪一个,该属性的值为你 3 bean 的名称。spring-doc.cadn.net.cn

持久Tokens方案

此方法基于文章 改进的持久登录 Cookie 最佳实践,并进行了少量修改 [1]。 若要在命名空间配置中使用此方法,您需提供数据源引用:spring-doc.cadn.net.cn

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

数据库应包含一个 persistent_logins 表,该表可通过以下 SQL(或等效语句)创建:spring-doc.cadn.net.cn

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)接口与实现

“记住我”(Remember-me)功能与 UsernamePasswordAuthenticationFilter 一起使用,并通过其父类 AbstractAuthenticationProcessingFilter 中的钩子(hooks)实现。 它也在 BasicAuthenticationFilter 中被使用。 这些钩子会在适当的时机调用一个具体的 RememberMeServices 实现。 以下代码清单展示了该接口:spring-doc.cadn.net.cn

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 且希望被记住时,将操作委托给具体实现。 此设计支持任意数量的“记住我”实现策略。spring-doc.cadn.net.cn

我们之前已经看到,Spring Security 提供了两种实现。 我们依次来看每一种实现。spring-doc.cadn.net.cn

基于Tokens的记住我服务

该实现支持在基于简单哈希的Tokens方法中描述的更简单的方法。 TokenBasedRememberMeServices生成一个RememberMeAuthenticationToken,由RememberMeAuthenticationProvider进行处理。 此认证提供者与TokenBasedRememberMeServices之间共享一个key。 此外,TokenBasedRememberMeServices需要一个UserDetailsService,以便从中检索用户名和密码以进行签名比较,并生成RememberMeAuthenticationToken以包含正确的GrantedAuthority实例。 TokenBasedRememberMeServices还实现了 Spring Security 的LogoutHandler接口,因此可与LogoutFilter配合使用,自动清除 Cookie。spring-doc.cadn.net.cn

默认情况下,此实现使用 SHA-256 算法对Tokens签名进行编码。 为了验证Tokens签名,将解析并使用从 algorithmName 中获取的算法。 如果未提供 algorithmName,则将使用默认的匹配算法,即 SHA-256。 您可以为签名编码和签名验证指定不同的算法,这样用户可以在安全地升级到不同的编码算法的同时,仍能验证旧的签名(当没有提供 algorithmName 时)。 为此,您可以将自定义的 TokenBasedRememberMeServices 指定为一个 Bean,并在配置中使用它。spring-doc.cadn.net.cn

@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
@Throws(Exception::class)
fun securityFilterChain(http: HttpSecurity, rememberMeServices: RememberMeServices): SecurityFilterChain {
    http
        .authorizeHttpRequests{ it.anyRequest().authenticated() }
        .rememberMe { it.rememberMeServices(rememberMeServices) }
    return http.build()
}

@Bean
fun rememberMeServices(userDetailsService: UserDetailsService): RememberMeServices {
    val encodingAlgorithm = RememberMeTokenAlgorithm.SHA256
    val rememberMe = TokenBasedRememberMeServices("myKey", userDetailsService, encodingAlgorithm)
    rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5)
    return rememberMe
}
<http>
    <intercept-url pattern="/**" access="authenticated"/>
    <remember-me services-ref="rememberMeServices"/>
</http>

<b:bean id="rememberMeServices"
        class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
    <b:constructor-arg value="myKey"/>
    <b:constructor-arg ref="userDetailsService"/>
    <b:constructor-arg value="SHA256"
                       type="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices$RememberMeTokenAlgorithm"/>
    <b:property name="matchingAlgorithm" value="MD5"/>
</b:bean>

要在应用程序上下文中启用“记住我”功能,需要以下 Bean:spring-doc.cadn.net.cn

@Bean
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) {
	return new TokenBasedRememberMeServices("myKey", userDetailsService);
}

@Bean
RememberMeAuthenticationFilter rememberMeFilter(AuthenticationManager authenticationManager,
		TokenBasedRememberMeServices rememberMeServices) {
	return new RememberMeAuthenticationFilter(authenticationManager, rememberMeServices);
}

@Bean
RememberMeAuthenticationProvider rememberMeAuthenticationProvider() {
	return new RememberMeAuthenticationProvider("myKey");
}
@Bean
fun rememberMeServices(userDetailsService: UserDetailsService): RememberMeServices {
    return TokenBasedRememberMeServices("myKey", userDetailsService)
}

@Bean
fun rememberMeFilter(authenticationManager: AuthenticationManager, rememberMeServices: TokenBasedRememberMeServices): RememberMeAuthenticationFilter {
    return RememberMeAuthenticationFilter(authenticationManager, rememberMeServices)
}

@Bean
fun rememberMeAuthenticationProvider(): RememberMeAuthenticationProvider {
    return RememberMeAuthenticationProvider("myKey")
}
<b:bean id="rememberMeServices"
            class="org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
    <b:constructor-arg value="myKey"/>
    <b:constructor-arg ref="userDetailsService"/>
</b:bean>

<b:bean id="rememberMeFilter"
            class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
    <b:constructor-arg ref="authenticationManager"/>
    <b:constructor-arg ref="rememberMeServices"/>
</b:bean>

<b:bean id="rememberMeAuthenticationProvider"
            class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
    <b:constructor-arg value="myKey"/>
</b:bean>

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

基于持久化Tokens的记住我服务

你可以像使用 TokenBasedRememberMeServices 一样使用此类,但此外还需要配置一个 PersistentTokenRepository 来存储Tokens。spring-doc.cadn.net.cn

有关数据库模式,请参见持久化Tokens方法spring-doc.cadn.net.cn


1. 本质上,用户名不会包含在 Cookie 中,以避免不必要地暴露有效的登录名。本文评论区对此有相关讨论。