这个是一个扩展功能,可以了解并在以后的项目中实践
其实现原理是用户在登录成功后的一段时间内,即使用户重新打开浏览器,或者重启服务器,都不需要用户重新登录了,用户依然可以直接访问接口获取数据
配置类
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and() //所有请求都需要认证才能访问.formLogin().rememberMe().and()//添加记住我功能.csrf().disable()//关闭csrf;}
默认的登陆页面上会出现一个记住我的复选框
接口传参多了一个参数remember_on: true
关闭session,再次新建窗口访问依然可以
后续的访问会带上cookie
单独提取出cookie的数据进行解析可知:
YWRtaW46MTYzMzg0MzM5OTM3MTo1NWRiOGI5OWY1OWM1NjhjMzNmOGNmNDdiMTc1YjU4MQ
这是一个Base64加密的字符串
我们对其进行解密
@Testvoid base64Dec() throws UnsupportedEncodingException {String rememberMe = "YWRtaW46MTYzMzg0MzM5OTM3MTo1NWRiOGI5OWY1OWM1NjhjMzNmOGNmNDdiMTc1YjU4MQ";String s = new String(Base64.getDecoder().decode(rememberMe), StandardCharsets.UTF_8);System.out.println("s = " + s);}
解密结果为:
s = admin:1633843399371:55db8b99f59c568c33f8cf47b175b581
可以看到,这段 Base64 字符串实际上用 : 隔开,分成了三部分:
- 第一段是用户名
- 第二段看起来是一个时间戳,我们通过在线工具或者 Java 代码解析后发现,这是一个两周后的数据。
解析如下: 2021-10-10 13:23:19 代码执行时间为2021-09-26
- 第三段我就不卖关子了,这是使用 MD5 散列函数算出来的值,他的明文格式是 username + “:” + tokenExpiryTime + “:” + password + “:” + key,最后的 key 是一个散列盐值,可以用来防治令牌被修改。
了解到 cookie 中 remember-me 的含义之后,那么我们对于记住我的登录流程也就很容易猜到了。
在浏览器关闭后,并重新打开之后,用户再去访问 hello 接口,
此时会携带着 cookie 中的 remember-me 到服务端,
服务到拿到值之后,可以方便的计算出用户名和过期时间,
再根据用户名查询到用户密码,
然后通过 MD5 散列函数计算出散列值,
再将计算出的散列值和浏览器传递来的散列值进行对比,就能确认这个令牌是否有效。
由于我们自己没有设置 key,key 默认值是一个 UUID 字符串,
这样会带来一个问题,就是如果服务端重启,这个 key 会变,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,
所以,我们可以指定这个 key。指定方式如下:
为了保证我们的记住登录一直有效,我们需要给他的存储信息设置一个固定的key值
@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and() //所有请求都需要认证才能访问.formLogin().rememberMe().key("zukxu").and()//添加记住我功能.csrf().disable()//关闭csrf;}
这样我们无论服务器是否重启,都可以保证记住登录在过期时间内一直有效
持久化
sql
但是我们应该如何保证cookie数据的安全性呢?
我们可以在数据库中新增一张表用作持久化存储
如果使用默认的 JDBC,即 JdbcTokenRepositoryImpl,我们可以来分析一下该类的定义:
public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implementsPersistentTokenRepository {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)";public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";}
可以根据源码推导出其默认的数据表sql, 如下:
CREATE TABLE `persistent_logins` (
`username` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`series` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
如果我们使用自定义的操作方式,例如jpa/mybatis等等,可以自己进行定义sql数据表,不必局限
配置类
提供一个 JdbcTokenRepositoryImpl 实例,并给其配置 DataSource 数据源,最后通过 tokenRepository 将 JdbcTokenRepositoryImpl 实例纳入配置中。
@Autowired
DataSource dataSource;
@Bean
JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and() //所有请求都需要认证才能访问
.rememberMe()//添加记住我功能
.key("zukxu")
.tokenRepository(jdbcTokenRepository())//添加持久化
.and()
.csrf().disable()//关闭csrf
;
}
测试
首先登录,并选择记住登录
登陆成功后重启服务器
再次进行访问
成功访问接口,
查看数据库,发现数据
二次校验
持久化数据之后已经能解决大多数的登录问题
但是我们还可以进行二次校验,将登录风险降至最低
即:
为了让用户使用方便,我们开通了自动登录功能,但是自动登录功能又带来了安全风险,
一个规避的办法就是如果用户使用了自动登录功能,我们可以只让他做一些常规的不敏感操作,例如数据浏览、查看,
但是不允许他做任何修改、删除操作,如果用户点击了修改、删除按钮,我们可以跳转回登录页面,让用户重新输入密码确认身份,
然后再允许他执行敏感操作。
例如:
添加一个接口
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
@GetMapping("/user/hello")
public String user() {
return "user";
}
@GetMapping("/rememberme")
public String rememberme() {
return "rememberme";
}
}
其中 【/rememberme】接口是使用了rememberme功能才能访问的接口,如果用户是通过用户名/密码认证的,则无法访问该接口。
配置相关权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/rememberme/**")
.rememberMe()//该接口只有使用了remember登录才能访问
.antMatchers("/admin/**")
//fullyAuthenticated 不同于 authenticated,fullyAuthenticated 不包含自动登录的形式,而 authenticated包含自动登录的形式。
.fullyAuthenticated()
//.hasRole("admin")
.antMatchers("/user/**")
.hasRole("user")
.anyRequest()
.authenticated()
.and() //所有请求都需要认证才能访问
rememberMe().key("zukxu").tokenRepository(jdbcTokenRepository()).and()//添加记住我功能
.csrf().disable()//关闭csrf
;
}
.antMatchers(“/rememberme/**”).rememberMe()//该接口只有使用了remember登录才能访问
fullyAuthenticated 不同于 authenticated,fullyAuthenticated 不包含自动登录的形式,而 authenticated 包含自动登录的形式。
测试
勾选了rememberMe进行登录才能访问接口
| 勾选 | ![]() |
|---|---|
| 不勾选 | ![]() |
其余接口只需登录即可进行访问


