实现记住我功能

1. 前言

「记住我」这一功能多出现在互联网应用中,其目的是为了减少用户的认证次数和访问门槛。在一般的内网应用、或者是安全性要求较高的管理后台中出现使用频度较低。

「记住我 Remember-me」也称为「持续登录 Persistent-login」, 主要用到了 Cookies 和 Token 技术,本节重点讨论如何通过 Spring Security 配合出「记住我」的自动认证功能。

2. 记住我原理

图片描述

「记住我」的核心思路是:将认证状态以安全的方式保存在客户端

「记住我」需要通过向浏览器设置 Cookies 信息,这个 Cookies 信息未来会用于建立会话连接,并且提供自动登录的能力。

「记住我」的基本流程为:

  1. 用户通过浏览器登录成功后,服务端生成一个可以持久化使用的 Token,并返回给浏览器;
  2. 浏览器端将该 Token 保存到 Cookies 中;
  3. 当用户离开应用系统,并再次返回,此时服务端由于没有了该用户的登录会话,所以要求用户再次登录;
  4. 浏览器检查 Cookies 中是否包含「记住我」的 Token,如有,将其发送给服务端;
  5. 服务端验证 Token,如果成功,直接返回登录成功的结果。

3. 集成步骤

3.1 「记住我」Token 的存储方式

通过前面描述我们看到,要实现「记住我」功能,关键在于如何安全的保护好用户的认证信息 Token。Spring Security 提供了两种「记住我」的实现方式:

  1. 使用 Hash 算法加密认证信息形成 Token,并将其保存在客户端中;

  2. 将认证信息保存在数据库中,并将查询条件保存在客户端中。

3.1.1 基于 Hash 的方式

基于 Hash 的方式是一种相对简单的集成方式。这种方式利用 Hash 的特性,将「记住我」信息进行存档。每当用户认证通过,服务端便生成一条 Hash 记录,并发送给客户端浏览器,其中内容包括「用户名」、「Token 过期时间」、「密码」、「签名秘钥」。

发送的具体内容为:

base64(username + ":" + expirationTime + ":" + md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username:          根据 UserDetailsService 配置得到用户名信息。
password:          认证密码,确保 UserDetailsService 中可以匹配到目标用户。
expirationTime:    「记住我」Roken 的有效期,精确到毫秒。
key:               用于给 Token 签名的密钥信息,防止该 Token 被篡改。

发送出的 Token 只有到用户下次需要登录时才会被使用到,这期间,需要确保用户名、密码、密钥等信息不被改变。还需要注意的是,「记住我」Token 在过期之前,可以在任何地方使用,因此其安全性上有一定的问题,如果使用数字认证一样。当用户认为自己的 Token 不在安全时,最好的办法是立刻改变自己的认证密码,并且使全部的「记住我」Token 失效。

启动「记住我」功能仅需要一行配置,具体方式为:

<http>
...
<remember-me key="签名密钥"/>
</http>

当有多个 UserDetailsService 实例时,可以通过 user-service-ref 属性指定唯一实例。

3.1.2 基于存储的方式

使用数据库作为 Token 存储方式,需要在 <remember-me> 配置中增加 data-source-ref 属性,配置方式如下:

<http>
...
<remember-me data-source-ref="数据源实例"/>
</http>

所用到的数据源需要包含 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)

3.2 「记住我」相关接口及其实现

「记住我」需要配合「用户名密码认证过滤器」一起使用,触发 RememberMeServices 实例实现其效果。「记住我」接口中有三个主要方法,第一个名为 autoLogin 用于自动登录审核,另外两个是 loginFailloginSuccess 分别在认证失败或成功时触发。

具体表现形式为:

// 自动认证
Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

自动认证方法,在「记住我」功能启用后,同时当前上下文中找不到用户信息时触发,我们需要根据不同的 Token 策略,实现「记住我」的判断逻辑。

// 登录失败时触发
void loginFail(HttpServletRequest request, HttpServletResponse response);
// 登录成功时触发
void loginSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication successfulAuthentication);

如前文所述,「记住我」有两种 Token 策略,对应了两种实现方法。

3.2.1 基于 Hash 方式的实现

先上代码:

<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>

基于 Hash 的方式需要配置三个核心 Bean 对象,分别是「过滤器」、「记住我处理服务」和「认证管理器」。这其中 TokenBasedRememberMeServices 负责生成 Token 内容,并交给「认证管理器」使用。

最后,要把处理服务 RememberMeServices 设置到用户名密码认证过滤器 UsernamePasswordAuthenticationFilter.setRememberMeServices() 里,将记住我的认证管理器添加到 AuthenticationManager.setProviders() 之中,将记住我过滤器添加到安全过滤链之中。

3.2.2 基于数据存储方式的实现

使用数据存储方式,其实现代码与 Hash 方式基本相同,区别在于需要继续配置 PersistentTokenRepository 来存取 Token,有两个标准实现类:第一个是基于内存的 InMemoryTokenRepositoryImpl,第二个是基于 JDBC 的 JdbcTokenRepositoryImpl。通常情况下,第一种用于集成测试,第二种用于生产环境。

4. 小结

本节我们讨论了「记住我」的原理及快速集成方式:

  • 「记住我」是一种基于 Token 的认证形式;
  • 「记住我」基于浏览器 Cookie 实现,在浏览器中保存从服务端获取的,用于下次认证的 Token 内容;
  • 「记住我」是需要和用户名密码认证方式同时出现;
  • 「记住我」有两种 Token 策略,一种基于 Hash 值,另外一种基于数据库持久化。

下节我们讨论,当系统对认证有特殊需求且无法由 Spring Security 安全框架提供时,如何实现使用外部方式认证,使用 Spring Security 管理认证结果及鉴权的方法。