持久化令牌
使用持久化令牌实现 RememberMe 的体验和使用普通令牌的登录体验是一样的,不同的是服务端所做的事情变了。持久化令牌在普通令牌的基础上,新增了 series 和 token 两个校验参数,当使用用户名/密码的方式登录时,series 才会自动更新;而一旦有了新的会话,token 就会重新生成。所以,如果令牌被人用,一旦对方基于 RememberMe 登录成功后,就会生成新的 token,你自已的登录令牌就会失效,这样就能及时发现账户泄漏并作出处理,比如清除自动登录令牌、通知用户账户泄漏等。
Spring Security 中对于持久化令牌提供了两种实现:JdbcTokenRepositoryImpl 和 InMemoryTokenRepositoryImpl,前者是基于 JdbcTemplate 来操作数据库,后者则是操作存储在内存中的数据。由于 InMemoryTokenRepositoryImpl 的使用场景很少,因此这里主要介绍基于 JdbcTokenRepositoryImpl 的配置。
我们在 6.2 小节的案例上继续完善。
首先创建一个 security06 数据库,然后我们需要一张表来记录令牌信息,创建表的SQL 脚本在在 JdbcTokenRepositoryImpl 类中的 CREATE_TABLE_SQL 变量上已经定义好了,代码如下:
public static final String CREATE_TABLE_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)";
我们直接将变量中定义的 SQL 脚本拷贝出来到数据库中执行,生成一张 persistent_logins 表用来记录令牌信息。persistent_logins 表一共就四个字段:username 表示登录用户名、series 表示生成的 series 字符串、token 表示生成的 token 字符串、last_used 则表示上次使用时间。
接下来,在项目中引入 JdbcTemplate 依赖和 MySQL 数据库驱动依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
然后在 application.properties 中配置数据库连接信息:
spring.datasource.url=jdbc:mysql:///security06?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123
最后修改 SecurityConfig:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
@Bean
PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("123")
.roles("admin");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").fullyAuthenticated()
.antMatchers("/rememberme").rememberMe()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.rememberMe()
// .key("javaboy")
.tokenRepository(jdbcTokenRepository())
.and()
.csrf().disable();
}
}
在配置中我们提供了一个 JdbcTokenRepositoryImpl 实例,并为其配置了数据源,最后在配置 RememberMe 时通过 tokenRepository 方法指定 JdbcTokenRepositoryImpl 实例。
配置完成后,启动项目并进行登录测试。登录成功后,我们发现数据库表中多了一条记录,如图6-5所示。

此时如果关闭浏览器重新打开,再去访问 /hello 接口,访问时并不需要登录,但是访问成功之后,数据库中的 token 字段会发生变化。同时,如果服务端重启之后,浏览器再去访问 /hello 接口,依然不需要登录,但是 token 字段也会更新,因为这两种情况中都有新会话的建立,所以 token 会更新,而 series 则不会更新。当然,如果用户注销登录,则数据库中和该用户相关的登录记录会自动清除。
可以看到,持久化令牌比前面的普通令牌安全系数提高了不少,但是依然存在风险。安全问题和用户的使用便捷性就像一个悖论,想要用户使用方便,不可避免地要牺牲一点安全性。对于开发者而言,要做的就是如何将系统存在的安全风险降到最低。
接下来我们就来看二次校验,继续提高 RememberMe 登录的安全性。