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

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 【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 的逻辑之后,即使不看这里的源码,你也一定想得到。

目录
相关文章
|
7天前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
7天前
|
安全 Java 数据库
实现基于Spring Security的权限管理系统
实现基于Spring Security的权限管理系统
|
7天前
|
安全 Java 数据安全/隐私保护
解析Spring Security中的权限控制策略
解析Spring Security中的权限控制策略
|
22天前
|
JSON 安全 Java
Spring Security 6.x 微信公众平台OAuth2授权实战
上一篇介绍了OAuth2协议的基本原理,以及Spring Security框架中自带的OAuth2客户端GitHub的实现细节,本篇以微信公众号网页授权登录为目的,介绍如何在原框架基础上定制开发OAuth2客户端。
44 4
Spring Security 6.x 微信公众平台OAuth2授权实战
|
25天前
|
存储 安全 Java
Spring Security 6.x OAuth2登录认证源码分析
上一篇介绍了Spring Security框架中身份认证的架构设计,本篇就OAuth2客户端登录认证的实现源码做一些分析。
51 2
Spring Security 6.x OAuth2登录认证源码分析
|
29天前
|
安全 Java 数据安全/隐私保护
Spring Security 6.x 一文快速搞懂配置原理
本文主要对整个Spring Security配置过程做一定的剖析,希望可以对学习Spring Sercurity框架的同学所有帮助。
63 5
Spring Security 6.x 一文快速搞懂配置原理
|
26天前
|
安全 Java API
Spring Security 6.x 图解身份认证的架构设计
【6月更文挑战第1天】本文主要介绍了Spring Security在身份认证方面的架构设计,以及主要业务流程,及核心代码的实现
25 1
Spring Security 6.x 图解身份认证的架构设计
|
8天前
|
安全 Java 数据安全/隐私保护
使用Spring Security实现细粒度的权限控制
使用Spring Security实现细粒度的权限控制
|
12天前
|
安全 Java 数据安全/隐私保护
使用Java和Spring Security实现身份验证与授权
使用Java和Spring Security实现身份验证与授权
|
14天前
|
存储 安全 Java
Spring Security在企业级应用中的应用
Spring Security在企业级应用中的应用