SpringSecurity源码 - 会话管理原理 并发会话限制、查看在线用户、踢人下线 源码解析

前言

"会话管理"在一个系统中是很常见的功能,例如:查看在线用户、踢人下线、账号只允许单人登录(包括两种限制策略:后来者顶掉前者、前者注销后才允许后来者登录)。在SpringSecurity中,实现了很完备的会话管理功能,本文来看一下SpringSecurity中是怎么使用会话管理的,以及对它的底层原理进行梳理。

使用

创建一个SpringBoot项目,版本v2.6.13,引入web与SpringSecurity依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

想要在SpringSecurity中使用会话管理功能,需要创建一个配置类,在配置类中进行相关配置:

上面的配置类中:

  1. configure(HttpSecurity)方法中:HttpSecurity用于配置一条过滤器链,super.configure(http)是调用了父类的方法,这里是保留了父类的过滤器链配置,这里不做改动。
  2. http.sessionManagement() 这是开启了会话管理配置,同时指定了maximumSessions的值为1(可自定义)。也就是同一用户的并发登录数为1。
  3. configure(AuthenticationManagerBuilder)方法中:提供一个内存中的用户对象。

配置完毕之后 访问项目:使用Chrome浏览器的多用户方式,来模拟两台电脑分别登录。

后来者顶掉前者

使用第二个浏览器登录之后,返回第一个窗口,刷新会显示报错信息,说明第一个窗口的登录已经被挤掉了。

后来者必须等前者注销

想要实现这种模式,需要修改一下配置类:添加一个配置项 设置为true

如图,第二个窗口登录时,由于第一个浏览器已经登录了,所以显示了一个错误信息,无法登录。

查看在线用户

要实现查看在线用户的功能,首先需要修改配置类:注册一个SessionRegistry 这个类是用来存储会话信息的,在这里把他注入容器,我们就可以在其它类比方说controller中,直接使用它获取会话信息了 其他类中:注入SessionRegistry,并调用其方法(方法及参数的含义后面会讲) 调用接口,返回了当前在线的所有用户的username

踢人下线

踢人下线本质就是让session过期:我们这里为了简便,就直接让当前登录用户的session过期

如图,登录之后,调用kickOut接口,再刷新页面,就弹出会话失效过期。

总结

上述演示中,效果可能比较简陋,这都不是问题。因为在实际开发中,我们通常会对这类效果进行美化,例如 当被其它用户顶掉时,可能弹出一个提示框"您的账号已在其它位置登录"、被踢下线时,可能会提示"您的登录状态已失效",然后跳转登录页面,由于这里只是为了展示功能的使用,所以就没有去做效果的美化。

组件介绍

在正式梳理源码之前,先来介绍一下SpringSecurity中,会话管理相关的一些组件,了解了这些组件,对于理解下面的源码解将会很有帮助。

SecurityConfigurer

在SpringSecurity中,整个认证流程,封装为了一条过滤器链,过滤器链中是一系列的过滤器,一个请求,会走完过滤器链的每个过滤器,所以针对请求的判断、操作等逻辑,都封装为了一个个的过滤器,封装到了过滤器链中。

而在SpringSecurity中,xxxConfigurer 格式、也就是以Configurer结尾 的类,一般是某个过滤器的配置器。这个配置器用于创建对应的过滤器,以及对它进行后置处理(一般是将创建好的过滤器,注入到spring容器中)。

所有的配置器都实现了SecurityConfigurer 接口,接口中定义了两个方法:过滤器的初始化方法init 以及配置方法configure。后续每一个过滤器的创建,都要调用这两个方法。简单看一下这个接口的定义:

java 复制代码
public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
	void init(B builder) throws Exception;
	void configure(B builder) throws Exception;
}

SecurityBuilder

这个类是SpringSecurity中的构建器对象,在SpringSecurity中,所有需要构建的对象都由SecurityBuilder对象来构建。接口中定义了一个build方法:

java 复制代码
public interface SecurityBuilder<O> {
	O build() throws Exception;
}

SecurityBuilder有很多个实现类,这里就看一下比较重要的:AbstractConfiguredSecurityBuilder

AbstractConfiguredSecurityBuilder就是一个构建器的抽象类,抽取了构建的公共逻辑。在这个类中,有一个成员变量configurers ,这是一个Map集合,里面封装的就是SecurityConfigurer对象,也就是上面提到的过滤器配置器。

这些配置器会在doBuild 方法中被调用,这个方法通过遍历configurers ,调用他们的init 方法以及configure方法,从而生产出对应的过滤器。如下源码中:分别遍历了所有的配置器,先调用了init方法完成初始化,后调用了configure方法完成配置。

到这里我们可以明白:SpringSecurity中的每个过滤器,都是由对应的配置器 创建的,这些配置器,实现了SecurityConfigurer接口,并且他们会被封装在AbstractConfiguredSecurityBuilder 的成员变量configurers 中,后续在SpringSecurity的初始化过程中,会调用到这个类的doBuild 方法,在这个方法中,会遍历configurers,取出所有的过滤器配置器,先调用init方法完成初始化,后调用configure方法完成配置,从而创建出对应的过滤器,并添加到过滤器链中。

SessionInformation

在SpringSecurity中,会话的记录工作由SessionInformation这个类来完成,先看一下这个类的结构:

如图,这个类中,定义了一些与会话管理相关的变量,例如会话id、会话是否过期、会话对应用户、会话是否过期,当前会话最后一次请求的时间等。

在前面演示功能时,查看在线用户,我们调用了SessionInformation的getPrincipal 方法,其实就是返回的这个类中的principal ,即当前会话对应的用户信息。踢人下线功能演示中,我们调用了expireNow方法,其实就是令当前会话过期,用户自然就会掉线。

SessionRegistry

SessionRegistry是一个接口,它的主要工作就是用来维护SessionInformation实例,该接口定义如下,方法名都简介明了,就不过多解释了,可参考注释:

java 复制代码
public interface SessionRegistry {
    //获取所有用户信息
	List<Object> getAllPrincipals();
    //根据用户信息,获取用户对应的所有SessionInformation对象
    //includeExpiredSessions意为是否包含已过期的
	List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);
    //根据会话id获取SessionInformation对象
	SessionInformation getSessionInformation(String sessionId);
    //根据会话id刷新最后一次请求时间
	void refreshLastRequest(String sessionId);
    //根据会话id和用户信息注册一个新会话  
	void registerNewSession(String sessionId, Object principal);
    //根据会话id删除一个会话
	void removeSessionInformation(String sessionId);
}

这个接口只有一个实现类SessionRegistryImpl,所以我们在配置类中返回的是SessionRegistryImpl。我们在查看在线用户功能演示中,就用到了这个类。

简单看一下SessionRegistryImpl中的方法:

首先看成员变量和构造器:

  1. 本类实现了ApplicationListener 接口,并且重写了onApplicationEvent方法,使得它可以监听HttpSession的销毁方法,进而移除Session记录。
  2. 本类中维护了了两个Map集合,分别为principalssessionIds,前者用于维护用户主体与SessionId之间的关系,后者用于维护SessionId与SessionInformation之间的关系。
  3. 在这里,principals 中的key是用户主体,所谓的用户主体实际就是SpringSecurity中定义的用户对象:org.springframework.security.core.userdetails.User ,在这个类中已经重写了equalshashcode方法,如果自定义用户对象,想使用会话管理,一定要重写这两个方法,否则会话管理可能会失效。

简单过一下剩余的方法:

SessionAuthenticationStrategy

这是一个接口,主要在用户登录成功后,对HttpSession进行处理。接口中定义了一个方法,用来处理和HttpSession相关的工作。

java 复制代码
public interface SessionAuthenticationStrategy {
	void onAuthentication(Authentication authentication, HttpServletRequest request, 
                    HttpServletResponse response) throws SessionAuthenticationException;
}

说白了,就是在用户登录成功之后,处理一系列的和会话相关的工作,可能处理会话并发、可能用来记录会话信息等等。这个接口的实现类很多,先简单了解下即可。

SessionManagementFilter

在SpringSecurity中,用来做会话管理的过滤器为SessionManagementFilter,它的doFilter方法中即为针对会话管理的逻辑。

SessionManagementConfigurer

前面提到了,每个过滤器都有一个对应的配置器,SessionManagementFilter的配置器就是SessionManagementConfigurer,在它的init方法中,完成了初始化逻辑,configure方法中,创建了SessionManagementFilter,并将其添加到了SpringSecurity的过滤器链中。

源码解析

配置类 sessionManagement

要知道SpringSecurity怎么实现的会话管理,我们先看一下,在配置类中,它做了什么。 配置类中会话管理的相关项如下:

sessionRegistry方法就不做过多解释,我们已经明白了SessionRegistry 对象用来维护SessionInformation实例,也就是说 里面封装了会话数据。我们在这里注入SessionRegistry,方便我们在项目的其它地方,调用它里面的方法,来获取会话相关信息。

重点还是看一下configure中的配置项,先看一下sessionManagement方法的调用:

可以看到,这一步做了两件事:

  1. 创建了过滤器SessionManagementFilter 的配置器SessionManagementConfigurer
  2. 调用了getOrApply方法,将创建好的配置器进行了传入。该方法源码如下:

可以看到,在这个方法中,首先调用了getConfigurer方法,这里其实调用的就是AbstractConfiguredSecurityBuilder 中的方法,具体逻辑就是判断变量configurers中有没有当前配置器。前面我们已经知道了,这个变量里面封装了所有过滤器的配置器,

那我们大胆猜测,这里要将上一步创建的配置器添加到这个集合中,方便后续创建过滤器。相关代码如下:

如果configurers 中没有当前配置器,调用apply方法,apply方法源码如下:

可以看到,apply方法中又调用了add方法,经过一系列的判断后,将配置器添加到了configurers 中。也就是说,sessionManagement这个方法的调用,就是创建了一个过滤器的配置器,添加到了configurers中,方便后续创建过滤器。

配置类 maximumSessions

这个方法定义了并发会话的最大数,比如,如果定义了1,那么同一个用户最多只允许一个会话存在,如果多人登录,后来者会顶掉前者(未配置maxSessionsPreventsLogintrue的前提下)。 我们看一下maximumSessions这个配置方法中做了什么。

因为上一步中返回了配置器对象,所以,这里的maximumSessions方法自然是SessionManagementConfigurer中的方法:

可以看出,这个方法中,首先将传入的maximumSessions赋值给了当前配置器中的一个变量,这个变量在配置器执行configure方法创建过滤器时,会把值设置到SessionAuthenticationStrategy的子类ConcurrentSessionControlAuthenticationStrategy 中,这个子类正是用来做会话并发管理的。

然后当前方法返回了一个ConcurrencyControlConfigurer 对象,需要注意,这个类虽然也以Configurer结尾,但它并未实现SecurityConfigurer接口,所以它不是某个过滤器的配置器,而是SessionManagementConfigurer中的一个内部类,主要保存了一些跟会话并发管理相关的配置项

配置类 maxSessionsPreventsLogin

这个方法决定了会话并发管理的形式,如果配置为true,那在超过会话并发最大数时,就会导致后来者无法登录,直到前者注销登录或会话过期。 因为上一步中返回了ConcurrencyControlConfigurer对象,所以这个方法自然就是这个类中的:

可以看到,这个方法中,其实就是将配置的值封装到了一个成员变量中,这个变量的值同样也会在配置器执行configure方法创建过滤器时,封装到ConcurrentSessionControlAuthenticationStrategy的某个变量中。

SessionManagementConfigurer

从上面的解析可以看出,ConcurrentSessionControlAuthenticationStrategy 就是用来处理会话并发的,因为会话并发相关的两个配置项,最终都配置到了这个类中。它的设置过程,是在配置器的configure 方法中。下面就来看一下配置器SessionManagementConfigurer的源码,来核实一下这个结论。

init方法

在init方法中,主要进行了一些共享对象的配置。例如:SecurityContextRepository、RequestCache、SessionAuthenticationStrategy、InvalidSessionStrategy等等,创建出这些对象,然后将他们设置到HttpSecurity的共享对象集合中,方便后续使用。

我们就详细来看一个 SessionAuthenticationStrategy对象的创建,看看它是怎么保存会话信息、处理会话并发等情况的。

具体的创建逻辑,是封装在了getSessionAuthenticationStrategy方法中,看一下源码:

首先先进行了一个判断,如果当前实例已不为Null,直接返回。同时还提供了一个ChangeSessionIdAuthenticationStrategy实例,用于防止会话固定攻击。

上面的代码中,分别创建了ConcurrentSessionControlAuthenticationStrategy、RegisterSessionAuthenticationStrategy实例,添加到了List集合delegateStrategies中,最后创建了CompositeSessionAuthenticationStrategy实例并返回,这里是应用了组合模式,将上述三个实例进行了代理。

也就是说,在用户认证成功后,先要进行修改SessionId以防止会话固定攻击(ChangeSessionIdAuthenticationStrategy )、然后 会进行会话并发相关的处理(ConcurrentSessionControlAuthenticationStrategy ),最后 会将会话信息保存到SessionRegistry中(RegisterSessionAuthenticationStrategy)。

configure方法

在这个方法中,主要进行了两个过滤器的创建,分别是SessionManagementFilter、ConcurrentSessionFilter。

上面的代码中,创建了过滤器SessionManagementFilter,并将其添加到了SpringSecurity的过滤器链中。 创建过程中,通过调用getSessionAuthenticationStrategy 方法,获取到了CompositeSessionAuthenticationStrategy对象,封装到了SessionManagementFilter中。

上面的代码中,先判断了当前是否开启了会话并发管理,其实就是判断配置项maximumSessions 是否不为null。如果已开启,就创建一个ConcurrentSessionFilter,添加到过滤器链中。 截止到这里,我们就能明白,在SessionManagementConfigurer的init及configure方法中,创建出了一系列的组件,并且创建了两个过滤器,添加到了SpringSecurity的过滤器链中。

下面,我们简单看一下这两个过滤器中的过滤逻辑。

过滤器过滤逻辑

SessionManagementFilter

先来看一下SessionManagementFilter的过滤逻辑,主要集中在doFilter方法中:

主要的逻辑就是判断当前用户是否已经认证成功,如果已成功,那么调用sessionAuthenticationStrategy的onAuthentication方法,进行后续的会话相关的处理。这里sessionAuthenticationStrategy其实就是CompositeSessionAuthenticationStrategy

ConcurrentSessionFilter

这个过滤器的逻辑也不复杂,主要就是先获取到当前会话,如果当前会话不为空,根据会话id获取到SessionInformation对象,如果SessionInformation对象不为空,判断它是否过期,如果已过期,调用doLogout进行注销操作,然后调用会话过期回调。

如果SessionInformation未过期,则刷新最后请求时间。

SessionAuthenticationStrategy逻辑

上面代码中,SessionAuthenticationStrategy为CompositeSessionAuthenticationStrategy,里面组合了三个实例,分别为ChangeSessionIdAuthenticationStrategy、ConcurrentSessionControlAuthenticationStrategy、RegisterSessionAuthenticationStrategy。下面看一下他们的源码,了解一下他们的运行机制。顺序就根据封装的顺序来。

CompositeSessionAuthenticationStrategy

这个是总的实例,它代理了下面三个实例。代码中就是遍历了它代理的实例集合,并分别调用了他们的onAuthentication 方法。

ChangeSessionIdAuthenticationStrategy

这个实例中代码很简单,就是先修改session的id,然后返回新的session。 需要注意,这个实例是从一个封装的抽象类AbstractSessionFixationProtectionStrategy中再次调用的。

ConcurrentSessionControlAuthenticationStrategy

这里的逻辑也比较简单

  1. 先获取到配置的最大并发数,如果为-1 说明未配置。不影响登录
  2. 如果当前已有的会话数小于配置的会话数,也不影响登录。注意,因为这里还未保存当前会话信息,所以是<,否则应为<=。
  3. 如果已达最大数,则判断当前会话是否已经存在于SessionInfomation集合中,如果是,也不影响登录。
  4. 如果上述都不满足,说明超过了最大并发数,调用allowableSessionsExceeded进行处理。

再看一下allowableSessionsExceeded方法的逻辑:

逻辑如下:

  1. 首先判断exceptionIfMaximumExceeded 的值,这个值实际就是配置类中配置的maxSessionsPreventsLogin。如果这里为true,说明不允许后来者顶掉前者,这里就会抛出异常,令后来者登陆失败。
  2. 如果允许后来者顶掉前者,就需要将最早的会话失效,具体操作就是先按照最后请求时间排序,再计算出需要失效的会话个数(一般是1),然后遍历并调用expireNow方法使其过期。

RegisterSessionAuthenticationStrategy

这里的逻辑也很简单,就是调用SessionRegistryregisterNewSession方法,注册一个新会话。

登录流程中的会话管理

截止到这里,所有关于会话管理组件的介绍及源码解析就完成了。最后还有一个问题,那就是用户在登录过程中,是怎么触发会话管理的呢?我们最后再来看一下这个小细节。

在之前介绍SpringSecurity登录流程的文章中,已经介绍过了,登录流程入口为AbstractAuthenticationProcessingFilter这个过滤器(不了解的朋友可 点击这里 查看)。

在这个过滤器中,认证成功之后,会调用成员变量sessionStrategyonAuthentication方法进行会话方面的处理。

但是我们发现,sessionStrategy的初始化值是一个NullAuthenticatedSessionStrategy,这是一个空实现,并不是我们前面介绍的组合模式的实例。

我们来到AbstractAuthenticationProcessingFilter 这个过滤器的配置器AbstractAuthenticationFilterConfigurer 中,找到配置方法configure,里面有一行关键代码。如下图

configure 方法中,会从传入的配置器对象中,获取共享对象,这里获取的是SessionAuthenticationStrategy类型,如果不为空,会将其取出,赋值到AbstractAuthenticationProcessingFilter的成员变量sessionStrategy中,这里也就解释了,为什么登录流程中能够正常的触发会话管理。

如果项目中没有配置会话管理,这里获取到的共享对象就是null ,就不会替换AbstractAuthenticationProcessingFilter中的sessionStrategy变量,默认就还是空实现NullAuthenticatedSessionStrategy,就不会执行任何会话管理逻辑。

总结

截止到这里,SpringSecurity中的会话管理功能已经全部解析完毕。在此做一个小总结:

用户登录成功后,会在AbstractAuthenticationProcessingFilterdoFilter 方法中触发会话管理(如果已配置会话管理的话),此时的SessionAuthenticationStrategy为CompositeSessionAuthenticationStrategy ,是一个组合模式的应用,它代理了三个SessionAuthenticationStrategy实例,分别为ChangeSessionIdAuthenticationStrategy (防止会话固定攻击)、ConcurrentSessionControlAuthenticationStrategy (会话并发相关的处理),RegisterSessionAuthenticationStrategy(将会话信息保存到SessionRegistry中)。由这三个对象完成了会话管理的功能。

以上就是本文的全部内容,感谢你的阅读,希望可以帮到你,谢谢!

相关推荐
计算机学姐2 小时前
基于python+django+vue的影视推荐系统
开发语言·vue.js·后端·python·mysql·django·intellij-idea
JustinNeil2 小时前
简化Java对象转换:高效实现大对象的Entity、VO、DTO互转与代码优化
后端
青灯文案12 小时前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
微尘83 小时前
C语言存储类型 auto,register,static,extern
服务器·c语言·开发语言·c++·后端
计算机学姐3 小时前
基于PHP的电脑线上销售系统
开发语言·vscode·后端·mysql·编辑器·php·phpstorm
码拉松4 小时前
千万不要错过,优惠券设计与思考初探
后端·面试·架构
魔术师卡颂5 小时前
如何让“学源码”变得轻松、有意义
前端·面试·源码
白总Server5 小时前
MongoDB解说
开发语言·数据库·后端·mongodb·golang·rust·php
计算机学姐5 小时前
基于python+django+vue的家居全屋定制系统
开发语言·vue.js·后端·python·django·numpy·web3.py
程序员-珍6 小时前
SpringBoot v2.6.13 整合 swagger
java·spring boot·后端