Spring Security 是如何“记住我”的?

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 【1月更文挑战第20天】这篇文章是这分析一下 Spring Security 的 RememberMe 功能,在阅读这篇文章之前,你需要了解如何配置 Spring Security 的基本的用户名/密码登录。

这篇文章是这分析一下 Spring Security 的 RememberMe 功能,在阅读这篇文章之前,你需要了解如何配置 Spring Security 的基本的用户名/密码登录,如果了解其中的原理就更好了。也欢迎参考我之前的一篇文章: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 名称。

从用户名/密码认证说起

这里需要你了解 Spring Security 的用户名/密码认证的原理,不了解的话可以参考我之前的文章(Spring Security 认证流程 )。

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

上面提到的的 rememberMeServicesRememberMeServices 类型,在 AbstractAuthenticationProcessingFilter 中是这样声明这个变量的:

private RememberMeServices rememberMeServices = new NullRememberMeServices();

RememberMeServices 这个接口,在 Spring Security 中只内置了两种直接的实现,上面代码中是默认使用的实现,其实就是不提供 RememberMe 功能时使用的实现,我们从 NullRememberMeServices 的名字中也能看得出来,实际上,它的所有方法实现都是空方法。

当我们在配置类中用 http.rememberMe() 开启了 RememberMe 功能后,这里的 rememberMeServices 会被替换成另一个实现,就是 AbstractRememberMeServicesAbstractRememberMeServices 是一个抽象类,它有两个非抽象的实现类,它们的层次结构是这样的:

image.png

这里 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);
}

代码中包含两个步骤:

  1. if 判断语句用来判断登录表单中的 remember-me 是不是被勾选了,如果没有,则直接返回。
  2. 如果勾选了的话,调用 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 的逻辑之后,即使不看这里的源码,你也一定想得到。

目录
相关文章
|
4天前
|
安全 Java 数据安全/隐私保护
|
8天前
|
安全 Java API
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)
28 0
第7章 Spring Security 的 REST API 与微服务安全(2024 最新版)(上)
|
1月前
|
存储 安全 Java
Spring Boot整合Spring Security--学习笔记
Spring Boot整合Spring Security--学习笔记
55 1
|
19天前
|
安全 数据安全/隐私保护
Springboot+Spring security +jwt认证+动态授权
Springboot+Spring security +jwt认证+动态授权
|
8天前
|
存储 安全 Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(下)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
18 2
|
8天前
|
安全 Cloud Native Java
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)(上)
第10章 Spring Security 的未来趋势与高级话题(2024 最新版)
23 2
|
8天前
|
安全 Java API
第5章 Spring Security 的高级认证技术(2024 最新版)(上)
第5章 Spring Security 的高级认证技术(2024 最新版)
34 0
|
8天前
|
存储 安全 Java
第3章 Spring Security 的用户认证机制(2024 最新版)(下)
第3章 Spring Security 的用户认证机制(2024 最新版)
32 0
|
8天前
|
存储 安全 Java
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(下)
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(下)
16 0
|
8天前
|
安全 Java 数据库
第2章 Spring Security 的环境设置与基础配置(2024 最新版)(上)
第2章 Spring Security 的环境设置与基础配置(2024 最新版)
34 0