Spring Security之多点登录控制

2024-09-04 14:20

本文主要是介绍Spring Security之多点登录控制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

上回我们聊了Spring Security之登录跳转,今天我们聊另外一个与功能相关的功能:多点登录控制。

多点登录控制

搞Web应用的同学都知道每个登录者的信息都是保存在Session之中,每个请求都会给带来一个唯一的sessionId。那么,这便是我们控制多点登录的切入口,或者说,基本实现思路。

假设A在C1电脑上登录,然后又在另外一台电脑C登录,那么在后台就会产生2个Session,如果我们把每个账号登录的Session记录下来,那不就能知道这个账号在几处登录了。那么,我们想要禁止账号多点登录——只能在一个地方登录——就可以限制住了。

SpringSecurity的实现

在Spring Security之Session管理中,我们在介绍的同时也聊到这个多点登录控制这个功能点,但并没有完全给大家串起来,因为那里的重点不是这个。今天咱就当复习,也来串一下流程。

多点登录控制流程分析

假设我们不允许多点登录,也就是只能在一个地方登录。

  1. 在电脑C1上登录成功,我们记录下登录成功的账号和sessionId。
  2. 再到电脑C2上登录,认证成功。但我们在校验当前登录的账号的session时,发现已经存在一个登录状态的session,且与当前session不一样。于是我们就抛出异常。(当然,我们也可以选择把第一个session设置为失效,即强制退出)
  3. 回到C1登出。将对应账号的session移除。
  4. 再到C2登录,校验通过,允许登录。

如果做的完善一些的话,这个功能还应该加上:

每次请求都检查session是否失效,然后移除账号session记录。

多点登录方案分析

基于上述流程分析结果,我们就能得到如下结论

  • 需要一个管理账号session信息的组件
  • 需要一个新的过滤器来检查每个请求的session是否有效。
  • 登录成功时,校验账号的session个数。校验通过,就登记,否则就拒绝。
  • 登出成功时,移除账号的session记录。

从上面的流程,我们可以看到,这个功能涉及到登录和登出。所以我们最好把这个功能抽离出来,而不是都放到登录里面。但很明显我们必须要在登录跟登出使用到同一个组件,不然这session记录就容易出问题。

Spring的实现

  • SessionRegistry
    负责维护账号的session信息。当前只有一个实现:SessionRegistryImpl

    /*** 值得注意的是,他监听了AbstractSessionEvent,* 可以通过session事件来管理session信息。* 而这些事件,则是SpringBoot内嵌的Tomcat发布的。* @see HttpSessionListener* @see HttpSessionEventPublisher*/
    public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {// <principal:Object,SessionIdSet>// 映射:登录的用户信息,sessionId集合private final ConcurrentMap<Object, Set<String>> principals;// <sessionId:Object,SessionInformation>// 映射:sessionId,session信息private final Map<String, SessionInformation> sessionIds;@Overridepublic void onApplicationEvent(AbstractSessionEvent event) {if (event instanceof SessionDestroyedEvent) {// 销毁session -- 通常是session超时SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event;String sessionId = sessionDestroyedEvent.getId();// 移除session信息removeSessionInformation(sessionId);}else if (event instanceof SessionIdChangedEvent) {// session变更 -- 登录成功后,sessionId会发生变化SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event;String oldSessionId = sessionIdChangedEvent.getOldSessionId();if (this.sessionIds.containsKey(oldSessionId)) {Object principal = this.sessionIds.get(oldSessionId).getPrincipal();// 移除老sessionremoveSessionInformation(oldSessionId);// 注册新sessionregisterNewSession(sessionIdChangedEvent.getNewSessionId(), principal);}}}}
    

    其最主要的接口行为:

    public interface SessionRegistry {// 获取某个账号/用户当前的所有sessionList<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);// 根据sessionId获取session信息SessionInformation getSessionInformation(String sessionId);// 注册新的sessionvoid registerNewSession(String sessionId, Object principal);// 刷新session时间void refreshLastRequest(String sessionId);// 移除sessionvoid removeSessionInformation(String sessionId);
    }
  • ConcurrentSessionFilter
    负责检查每个请求的session是否过期,如果过期,则从session注册器中移除。若没过期,就刷新时间。这样看的话,这个命名似乎有那么一点问题,因为他关注的是session是否过期。

    public class ConcurrentSessionFilter extends GenericFilterBean {private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws IOException, ServletException {HttpSession session = request.getSession(false);if (session != null) {// 通过session注册器获取session信息SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());if (info != null) {if (info.isExpired()) {// session已失效/过期,调用登出:就是要清理session信息doLogout(request, response);// 通过SessionInformationExpiredStrategy处理超时this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));return;}// 没失效就刷新时间this.sessionRegistry.refreshLastRequest(info.getSessionId());}}chain.doFilter(request, response);}private void doLogout(HttpServletRequest request, HttpServletResponse response) {// 获取认证信息Authentication auth = this.securityContextHolderStrategy.getContext().getAuthentication();// 调用登出处理器this.handlers.logout(request, response, auth);}
    }
    
  • SessionAuthenticationStrategy
    在聊Session管理的时候对这个组件详细了解过。既然如此就跟大家一起复习一下。
    实际上,SpringSecurity构建了两个不同的组件来协助我们完成这一session并发控制功能。详见我们的Session管理SessionAuthenticationStrategy这一小节。

    1. ConcurrentSessionControlAuthenticationStrategy
      负责校验并发数量。
    2. RegisterSessionAuthenticationStrategy
      负责注册新的session。

不过,这区分之后,两个session策略的执行顺序就必须是先校验再注册。实际上,SpringSecurity也是如此配置的。感兴趣的不妨再回头看下Session管理的《SessionManagementConfigurer》这一小结。

小结

眼尖的同学可能发现了,既然SessionRegistryImpl都监听了session事件,那为啥还要ConcurrentSessionFilter干啥?SessionRegitryImpl通过事件不就能实现刷新session记录时间和移除过期session了。
这个问题,是因为默认情况下,spring mvc的HttpSessionEventPublisher组件并不会声明,因此没有作用,自然不会发布相关事件。
要生效也简单,只需要声明一下就行。

	@Beanpublic HttpSessionEventPublisher httpSessionEventPublisher() {return new HttpSessionEventPublisher();}

完整的流程

假设我们配置最大的session个数为1,也就是不允许多点登录。

  1. 用户在C1登录
  2. 进入认证过滤器UsernamePasswordAuthenticationFilter,登录成功,使用SessionAuthenticationStrategy处理session。于是,经过ConcurrentSessionControlAuthenticationStrategy校验并发数量通过。再经过RegisterSessionAuthenticationStrategy将session登记到SessionRegistry
  3. 用户再到C2登录
  4. 重复步骤2,但ConcurrentSessionControlAuthenticationStrategy检查发现超出数量限制。于是当前登录被拒绝,并抛出SessionAuthenticationException
  5. ExceptionTranslationFilter检测到认证异常,重置用户登录态。包括SecurityContext,并跳转到登录页面。

后记

给大家一个任务,如果让你不使用框架,自己写过滤器实现单点登录功能,会怎么做呢?不妨参考上面的流程试试看?

下次,我准备跟大家聊聊RememberMe功能。

这篇关于Spring Security之多点登录控制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1136220

相关文章

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在

Java中ArrayList和LinkedList有什么区别举例详解

《Java中ArrayList和LinkedList有什么区别举例详解》:本文主要介绍Java中ArrayList和LinkedList区别的相关资料,包括数据结构特性、核心操作性能、内存与GC影... 目录一、底层数据结构二、核心操作性能对比三、内存与 GC 影响四、扩容机制五、线程安全与并发方案六、工程

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

Spring AI集成DeepSeek的详细步骤

《SpringAI集成DeepSeek的详细步骤》DeepSeek作为一款卓越的国产AI模型,越来越多的公司考虑在自己的应用中集成,对于Java应用来说,我们可以借助SpringAI集成DeepSe... 目录DeepSeek 介绍Spring AI 是什么?1、环境准备2、构建项目2.1、pom依赖2.2