Spring Security 是如何“记住我”的?
RememberMe 的配置
RememberMe 功能,相信你在很多网站都见过,简单讲,就是可以让网站在一段时间内“记住我”,免除每次都需要填写用户名/密码登录的麻烦。
这样的功能通常需要在 Cookie 中存放一个 Token 字符串(或者类似的东西),服务端通过这个 Token 解析对应的用户信息和失效,从而实现自动登录。
在 Spring Security 中,给我们提供了这个功能,默认是关闭的。如果需要在一个配置好了 Spring Security 并且提供了用户名/密码表单登录的工程上,开启 RememberMe,你需要做这么几件事儿:
第一步,在 Spring Security 的配置类中,添加如下的内容:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.rememberMe()
// .tokenRepository(persistentTokenRepository())
// .userDetailsService(userDetailsService)
// .userDetailsService(userDetailsService)
// .tokenValiditySeconds(1209600)
// 更多配置。。。
}
以上代码中隐藏了其余配置的部分,其中,最关键的就是 rememberMe()
方法,使得 RememberMeAuthenticationFilter
被加入 Spring Security 的过滤器链,并完成相关的功能,这里的细节,后续再去分析。
之后的被注释的代码,这些方法是我们对这个功能进行自定义的内容,因此不是必需的,后面我们遇到相关内容的时候再讲。
第二步,如果你自定义了登录表单,需要在表单中增加一个复选框。
<input name="remember-me" type="checkbox" />记住我</td>
这里的「记住我」三个字,可以随意写,能表达意思即可。但是 input
标签的 name
属性中的 remember-me
属性值,默认情况下是 Spring Security 规定好的,它会作为这里的表单参数名,也会作为 RememberMe 功能需要用到的 Cookie 名称。
从用户名/密码认证说起
UsernamePasswordAuthenticationFilter
过滤器的 doFilter
方法在其父类 AbstractAuthenticationProcessingFilter
中实现,在方法中,如果用户信息通过了认证,会调用 successfulAuthentication
方法,处理之后的逻辑,我们看一下这个方法的代码:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
之前的文章里说过,这里会把成功认证后的信息保存在 SecurityContext 中,之后,在代码的第 7 行,调用了 this.rememberMeServices.loginSuccess
方法,这便是与 RememberMe 有关的代码。
RememberMeServices
上面提到的的 rememberMeServices
是 RememberMeServices
类型,在 AbstractAuthenticationProcessingFilter
中是这样声明这个变量的:
private RememberMeServices rememberMeServices = new NullRememberMeServices();
RememberMeServices
这个接口,在 Spring Security 中只内置了两种直接的实现,上面代码中是默认使用的实现,其实就是不提供 RememberMe 功能时使用的实现,我们从 NullRememberMeServices
的名字中也能看得出来,实际上,它的所有方法实现都是空方法。
当我们在配置类中用 http.rememberMe()
开启了 RememberMe 功能后,这里的 rememberMeServices
会被替换成另一个实现,就是 AbstractRememberMeServices
。AbstractRememberMeServices
是一个抽象类,它有两个非抽象的实现类,它们的层次结构是这样的:
这里 AbstractRememberMeServices
的两个非抽象子类,Spring Security 的 RememberMe 的功能到底会使用哪个,取决于我们的配置。
- 默认情况下会使用
TokenBasedRememberMeServices
,提供了基础的功能。 - 如果我们在开启 RememberMe 功能的时候,同时配置了一个
PersistentTokenRepository
,那么 Spring Security 会自动选择PersistentTokenBasedRememberMeServices
的实现。这样的配置表示我们会使用持久化的方式保存 RememberMe 功能用到的 Token。这一部分的细节会在下一篇文章中介绍。
RememberMeToken 的保存
言归正传,我们看 loginSuccess
方法的具体实现,它是在 AbstractRememberMeServices
中实现的,代码如下:
@Override
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
if (!rememberMeRequested(request, this.parameter)) {
this.logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
代码中包含两个步骤:
if
判断语句用来判断登录表单中的remember-me
是不是被勾选了,如果没有,则直接返回。- 如果勾选了的话,调用
onLoginSuccess
方法,执行之后的逻辑。
onLoginSuccess
方法是在 AbstractRememberMeServices
的两个子类中实现的,代码逻辑不复杂,我简单来介绍一下:
- 在
TokenBasedRememberMeServices
实现类中,会将用户的信息、过期时间、签名,放到 Cookie 当中。 - 在
PersistentTokenBasedRememberMeServices
实现类中,除了将这些信息放入 Cookie 之外,还会通过PersistentTokenRepository
进行持久化。
至此,就完成了「记住我」的步骤,接下来看一下,当之前的用户名/密码认证信息失效后,我再次发送请求,Spring Security 如何能够「记得我」并「想起我」。
RememberMeAuthenticationFilter
这里主要的工作就是 RememberMeAuthenticationFilter
来处理的。当我们在配置类中开启 RememberMe 功能的时候,RememberMeAuthenticationFilter
过滤器就被加入了 Spring Security 的过滤器链当中。
我们找到这个过滤器,查看其 doFilter
方法:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
this.logger.debug(LogMessage
.of(() -> "SecurityContextHolder not populated with remember-me token, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
chain.doFilter(request, response);
return;
}
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
this.logger.debug(LogMessage.of(() -> "SecurityContextHolder populated with remember-me token: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'"));
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
}
if (this.successHandler != null) {
this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
return;
}
}
catch (AuthenticationException ex) {
this.logger.debug(LogMessage
.format("SecurityContextHolder not populated with remember-me token, as AuthenticationManager "
+ "rejected Authentication returned by RememberMeServices: '%s'; "
+ "invalidating remember-me token", rememberMeAuth),
ex);
this.rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, ex);
}
}
chain.doFilter(request, response);
}
代码虽然不短,但是逻辑很简单。
- 先看 SecurityContext 中是不是已经存在认证信息,也就是 Authentication,存在的话,就直接过,去过滤器链中的下一个过滤器。否则,接着向下执行。
- 接下来,通过
rememberMeServices
获取 Authentication,如果能获取到的话,调用authenticationManager.authenticate
方法去认证,并把成功认证后的信息保存到 SecurityContext 中。
至于怎样从 rememberMeServices
获取 Authentication,了解了保存 RememberMeToken 的逻辑之后,即使不看这里的源码,你也一定想得到。
转载自:https://juejin.cn/post/7057517668080812046