实现记住我功能
1. 前言
「记住我」这一功能多出现在互联网应用中,其目的是为了减少用户的认证次数和访问门槛。在一般的内网应用、或者是安全性要求较高的管理后台中出现使用频度较低。
「记住我 Remember-me」也称为「持续登录 Persistent-login」, 主要用到了 Cookies 和 Token 技术,本节重点讨论如何通过 Spring Security 配合出「记住我」的自动认证功能。
2. 记住我原理
「记住我」的核心思路是:将认证状态以安全的方式保存在客户端。
「记住我」需要通过向浏览器设置 Cookies 信息,这个 Cookies 信息未来会用于建立会话连接,并且提供自动登录的能力。
「记住我」的基本流程为:
- 用户通过浏览器登录成功后,服务端生成一个可以持久化使用的 Token,并返回给浏览器;
- 浏览器端将该 Token 保存到 Cookies 中;
- 当用户离开应用系统,并再次返回,此时服务端由于没有了该用户的登录会话,所以要求用户再次登录;
- 浏览器检查 Cookies 中是否包含「记住我」的 Token,如有,将其发送给服务端;
- 服务端验证 Token,如果成功,直接返回登录成功的结果。
3. 集成步骤
3.1 「记住我」Token 的存储方式
通过前面描述我们看到,要实现「记住我」功能,关键在于如何安全的保护好用户的认证信息 Token。Spring Security 提供了两种「记住我」的实现方式:
-
使用 Hash 算法加密认证信息形成 Token,并将其保存在客户端中;
-
将认证信息保存在数据库中,并将查询条件保存在客户端中。
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
用于自动登录审核,另外两个是 loginFail
和 loginSuccess
分别在认证失败或成功时触发。
具体表现形式为:
// 自动认证
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 管理认证结果及鉴权的方法。