SpringSecurity5 记住我功能

回到序章

来来来,点这

前言

在使用其他网站的时候,像 Gitee、博客园啥的,上面都有一个“记住我”的复选框,点击之后,即时后面关掉浏览器,甚至关机后再重启,也不用登陆就可以直接用。spring security 自然也有实现了这个,本章就介绍这个。

代码实现

存储在内存

Security 配置类在 configure(HttpSecurity http) 方法中找个位置增加代码

1
.rememberMe().and()

前端网页增加记住我的复选框,关键是 name 值必须等于 remmember-me

1
记住密码: <input type="checkbox" name="remember-me" /><br />

如果想要更改这里的 name 值,配置类也需要对应的更新

1
.rememberMe().rememberMeParameter("你想用的name值").and()

在登陆的时候勾上复选框,即可实现。

下面是在浏览器检查元素中的部分截图。

spring-security-remember-me-勾选前登陆请求

spring-security-remember-me-勾选后登陆请求

也可以进检查元素 - Application - Cookies,查看到对应的cookie token,该 token 通过 Base64 加密

spring-security-remember-me-token截图

其中的 token 是 dXNlcjE6MTYyMTU3ODQzNDMwMjo2YWRmNWI5ZjEzM2QyNzdlYWYzM2Q2M2JmMDQ1NmRkYw,使用 Base64 解密后为 user1:1621578434302:6adf5b9f133d277eaf33d63bf0456ddc

其中 user1 是登陆的账号;1621578434302 是过期时间,转换成能理解的格式是 2021-05-21,现在的时间是 2021-05-07,也就是默认是 14 天过期,需要重新登陆;6adf5b9f133d277eaf33d63bf0456ddc 是一串加密后的东东,直接给源码看吧。

1
2
3
4
5
6
7
8
9
10
protected String makeTokenSignature(long tokenExpiryTime, String username, String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + this.getKey();

try {
MessageDigest digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
} catch (NoSuchAlgorithmException var7) {
throw new IllegalStateException("No MD5 algorithm available!");
}
}

这部分源码下图的类中可以找到:

spring-security-remember-me-token生成最后一位规则代码截图

默认的 cookie-name 是 remember-me,默认的有效期是 14 天,这些都可以在配置类中配置更改:

1
2
3
4
5
6
.rememberMe()
// 更改 cookie-name 是 rmec
.rememberMeCookieName("rmec")
// 更改有效期为 1 天
.tokenValiditySeconds(24 * 60 * 60)
.and()

这个的有效期不能设置为永久,从源码分析,当设置的值小于 0 的时候,会设置为 2 周,源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
String username = retrieveUserName(successfulAuthentication);
String password = retrievePassword(successfulAuthentication);
// If unable to find a username and password, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password);
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}

源码位置:

spring-security-remember-me-token有效期规则截图

数据库持久化

配置类

1
2
3
.rememberMe()
.tokenRepository(xxxxx)
.and()

通过下面的这个方法可以看到,需要传入的是 org.springframework.security.web.authentication.rememberme.PersistentTokenRepository

1
2
3
4
public RememberMeConfigurer<H> tokenRepository(PersistentTokenRepository tokenRepository) {
this.tokenRepository = tokenRepository;
return this;
}

进入 PersistentTokenRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* The abstraction used by {@link PersistentTokenBasedRememberMeServices} to store the
* persistent login tokens for a user.
*
* @author Luke Taylor
* @since 2.0
* @see JdbcTokenRepositoryImpl
* @see InMemoryTokenRepositoryImpl
*/
public interface PersistentTokenRepository {

void createNewToken(PersistentRememberMeToken token);

void updateToken(String series, String tokenValue, Date lastUsed);

PersistentRememberMeToken getTokenForSeries(String seriesId);

void removeUserTokens(String username);

}

这个类的注释也简单明了的,这里是要实现持久化,所以选 JdbcTokenRepositoryImpl,进到这个类里面,可以看到建表以及增删改查的语句。

首先建议是直接拷建表语句去运行,因为它要生成表,你得设置 setCreateTableOnStartup(true) 才行,但是建完表得删除掉这句,因为语句是 create,所以当你第二次启动项目后,就会报错说该表已存在。

然后,默认的数据源是空,得赋值,所以得通过下面的写法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class SecurityWebConfiguration extends WebSecurityConfigurerAdapter {

@Autowired
private DataSource dataSource;

private JdbcTokenRepositoryImpl jdbcTokenRepository() {
JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
repository.setDataSource(dataSource);
return repository;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
...
.rememberMe().tokenRepository(jdbcTokenRepository())
...
;
}
}

分析

在介绍认证流程时,Emm,点进去直接滑到最底底,在处理成功/失败里,有那么一句 this.rememberMeServices.loginSuccess(request, response, authResult);

一层一层往上找,分别会找到如下

  • org.springframework.security.web.authentication.RememberMeServices.loginSuccess
    • org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices.loginSuccess
      • org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices.onLoginSuccess( 内存 )
      • org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices.onLoginSuccess( 持久化 )

这俩就大概跟你说了,你如果用 remember-me 登陆了,登陆成功后,会干些啥,大概就是该存库存库,该存 session 存 session,没啥好说的。另外退出登陆( /logout ),登陆失败,都是同样方法去找即可。

这里需要再介绍的是,当你关闭浏览器后,再次打开浏览器,访问依旧不用重新登陆是怎么实现的。

首先,就像登陆检验账号一样,都是通过过滤器实现的,而 remember-me 是 org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter 这个,大概核心的代码如下:

RememberMeAuthenticationFilter-doFilter

如果没法通过 SecurityContextHolder.getContext().getAuthentication() 获得当前的登陆账号信息,则取调用 autoLogin() 的方法

AbstractRememberMeServices-autoLogin

AbstractRememberMeServices-extractRememberMeCookie

autoLogin() 干的事也简单,先去获得 cookie,然后通过 processAutoLoginCookie() 这个方法去上面最底层的那俩个类,对应到 processAutoLoginCookie() 进行登陆。

TokenBasedRememberMeServices-processAutoLoginCookie

PersistentTokenBasedRememberMeServices-processAutoLoginCookie