18. 记住我身份验证

18.1 概述

记住我或持续登录身份验证指的是能够在会话之间记住主体的身份的网站。这通常是通过向浏览器发送 cookie 来完成的,在将来的会话中检测到 cookie 并导致自动登录。Spring Security 为这些操作提供了必要的挂钩,并且具有两个具体的记住我实现。一种使用散列来保持基于 cookie 的令牌的安全性,另一种使用数据库或其他持久存储机制来存储生成的令牌。

请注意,这两种实现都需要 UserDetailsService。如果你使用的身份验证提供程序不使用 UserDetailsService(例如,LDAP 提供程序),那么除非在应用程序上下文中还有 UserDetailsService bean,否则它不会工作。

18.2 简单的基于散列的令牌方法

这种方法使用散列来实现有用的记忆策略。本质上,一个 cookie 在成功的交互式认证后被发送到浏览器,cookie 的组成如下:

base64(username + ":" + expirationTime + ":" +
md5Hex(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

因此,只要用户名、密码和密钥不改变,记住我令牌仅对指定的时间段有效。值得注意的是,这存在一个潜在的安全问题,因为捕获的记住我令牌在令牌到期之前可从任何用户代理使用。这是与摘要身份验证相同的问题。如果主体知道捕获了令牌,那么它们可以轻松地更改密码,并立即使所讨论的所有记住我令牌无效。如果需要更显著的安全性,则应该使用下一节中描述的方法。另外,记住我服务根本不应该使用。

如果你熟悉命名空间配置一章中讨论的主题,那么只需添加 <remember-me> 元素就可以启用记住我身份验证:

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

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

18.3 持久令牌方法

此方法基于文章 http://jaspan.com/improved_persistent_login_cookie_best_practice 稍作修改 [16]。若要使用命名空间配置的这种方法,你将提供一个数据源引用:

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

数据库应该包含使用以下 SQL(或等效)创建的 persistent_logins 表:

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

18.4 记住我接口和实现

记住我与 UsernamePasswordAuthenticationFilter 一起使用,并通过 AbstractAuthenticationProcessingFilter 超类中的钩子实现。它也被用于基本身份验证过滤器中。钩子会在适当的时候唤起一个具体的 RememberMeServices。接口看起来像这样:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

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

请参考Javadoc,以便更全面地讨论这些方法的作用,不过请注意,在这个阶段 AbstractAuthenticationProcessingFilter 只调用 loginFail() 和 loginSuccess() 方法。每当 SecurityContextHolder 不包含 Authentication 时,RememberMeAuthenticationFilter 就调用 autoLogin() 方法。因此,此接口为底层的记住我实现提供与身份验证相关的事件的充分通知,并且每当候选 web 请求可能包含 cookie 并希望被记住时,就向该实现委托。这种设计允许任何数量的记住我的实现策略。我们已经看到,Spring Security 提供了两种实现方式。我们将依次研究这些问题。

18.4.1 TokenBasedRememberMeServices

此实现支持在 小节 18.2, “简单的基于散列的令牌方法” 描述的更简单的方法。TokenBasedRememberMeServices 生成一个 RememberMeAuthenticationToken,该令牌由 RememberMeAuthenticationProvider 处理。在这个身份验证提供者和 TokenBasedRememberMeServices 之间共享一个 key。此外,TokenBasedRememberMeServices 需要一个 UserDetailsService,它可以从中检索用户名和密码,以便进行签名比较,并生成 RememberMeAuthenticationToken,以包含正确的 GrantedAuthority。应用程序将提供某种类型的注销命令,如果用户请求此命令则会使 cookie 无效。TokenBasedRememberMeServices 还实现了 Spring Security 的 LogoutHandler 接口,因此可以与 LogoutFilter 一起使用以自动清除 cookie。

应用程序上下文中需要启用记住我服务的 bean 如下:

<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>

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

18.4.2 PersistentTokenBasedRememberMeServices

这个类可以以与 TokenBasedRememberMeServices 相同的方式使用,但是还需要配置一个 PersistentTokenRepository 来存储令牌。有两种标准实现。

  • InMemoryTokenRepositoryImpl 只用于测试。
  • JdbcTokenRepositoryImpl 将令牌存储在数据库中。

在 小节 18.3, “持久令牌方法” 中描述了数据库模式。



[16] 基本上,用户名不包含在 cookie 中,以防止不公开地显示有效的登录名。本文的评论部分对此进行了讨论。