1.需求分析

在同一个系统中,我们可能只允许一个用户在一个终端上登录,一般来说这可能是出于安全方面的考虑,但是也有一些情况是出于业务上的考虑,需求就是业务原因要求一个用户只能在一个设备上登录。

要实现一个用户不可以同时在两台设备上登录,我们有两种思路:

  • 后来的登录自动踢掉前面的登录。
  • 如果用户已经登录,则不允许后来者登录。

这种思路都能实现这个功能,具体使用哪一个,还要看我们具体的需求。

在 Spring Security 中,这两种都很好实现,一个配置就可以搞定。

2.具体实现

2.1 踢掉已经登录用户

想要用新的登录踢掉旧的登录,我们只需要将最大会话数设置为 1 即可,配置如下:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1);
}

maximumSessions 表示配置最大会话数为 1,这样后面的登录就会自动踢掉前面的登录。这里其他的配置都是我们前面文章讲过的,我就不再重复介绍,配置完成后,用 Chrome 浏览器进行测试(可以利用 Chrome 中隐私模式登录模拟多用户功能)。

  1. Chrome 上登录成功后,访问 /hello 接口。
  2. 利用 Chrome隐私模式模式上登录成功后,访问 /hello 接口。
  3. 在 Chrome 上再次访问 /hello 接口,此时会看到如下提示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

可以看到,这里说这个 session 已经过期,原因则是由于使用同一个用户进行并发登录。
注意:如果你自定义用户user直接实现UserDetails接口的实体类,需要将复写toString、hashCode、equals 函数,不然看不到效果,原因也很简单,不复写两个用户实例对象比较会有问题!代码如下:

    @Overridepublic String toString() {return this.username;}@Overridepublic int hashCode() {return username.hashCode();}@Overridepublic boolean equals(Object obj) {return this.toString().equals(obj.toString());}

2.2 禁止新的登录

如果相同的用户已经登录了,你不想踢掉他,而是想禁止新的登录操作,那也好办,配置方式如下:

@Override
protected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}

添加 maxSessionsPreventsLogin 配置即可。此时一个浏览器登录成功后,另外一个浏览器就登录不了了。是不是很简单?

不过还没完,我们还需要再提供一个 Bean:

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

为什么要加这个 Bean 呢?因为在 Spring Security 中,它是通过监听 session 的销毁事件,来及时的清理 session 的记录。用户从不同的浏览器登录后,都会有对应的 session,当用户注销登录之后,session 就会失效,但是默认的失效是通过调用 StandardSession#invalidate 方法来实现的,这一个失效事件无法被 Spring 容器感知到,进而导致当用户注销登录之后,Spring Security 没有及时清理会话信息表,以为用户还在线,进而导致用户无法重新登录进来(小伙伴们可以自行尝试不添加上面的 Bean,然后让用户注销登录之后再重新登录)。

为了解决这一问题,我们提供一个 HttpSessionEventPublisher ,这个类实现了 HttpSessionListener 接口,在该 Bean 中,可以将 session 创建以及销毁的事件及时感知到,并且调用 Spring 中的事件机制将相关的创建和销毁事件发布出去,进而被 Spring Security 感知到,该类部分源码如下:

public void sessionCreated(HttpSessionEvent event) {HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession());getContext(event.getSession().getServletContext()).publishEvent(e);
}
public void sessionDestroyed(HttpSessionEvent event) {HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession());getContext(event.getSession().getServletContext()).publishEvent(e);
}

OK,虽然多了一个配置,但是依然很简单!

3.实现原理

上面这个功能,在 Spring Security 中是怎么实现的呢?我们来稍微分析一下源码。

首先我们知道,在用户登录的过程中,会经过 UsernamePasswordAuthenticationFilter(参考:手把手带你捋一遍 Spring Security 登录流程),而 UsernamePasswordAuthenticationFilter 中过滤方法的调用是在 AbstractAuthenticationProcessingFilter 中触发的,我们来看下 AbstractAuthenticationProcessingFilter#doFilter 方法的调用:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) req;HttpServletResponse response = (HttpServletResponse) res;if (!requiresAuthentication(request, response)) {chain.doFilter(request, response);return;}Authentication authResult;try {authResult = attemptAuthentication(request, response);if (authResult == null) {return;}sessionStrategy.onAuthentication(authResult, request, response);}catch (InternalAuthenticationServiceException failed) {unsuccessfulAuthentication(request, response, failed);return;}catch (AuthenticationException failed) {unsuccessfulAuthentication(request, response, failed);return;}// Authentication successif (continueChainBeforeSuccessfulAuthentication) {chain.doFilter(request, response);}successfulAuthentication(request, response, chain, authResult);

在这段代码中,我们可以看到,调用 attemptAuthentication 方法走完认证流程之后,回来之后,接下来就是调用 sessionStrategy.onAuthentication 方法,这个方法就是用来处理 session 的并发问题的。具体在:

public class ConcurrentSessionControlAuthenticationStrategy implementsMessageSourceAware, SessionAuthenticationStrategy {public void onAuthentication(Authentication authentication,HttpServletRequest request, HttpServletResponse response) {final List<SessionInformation> sessions = sessionRegistry.getAllSessions(authentication.getPrincipal(), false);int sessionCount = sessions.size();int allowedSessions = getMaximumSessionsForThisUser(authentication);if (sessionCount < allowedSessions) {// They haven't got too many login sessions running at presentreturn;}if (allowedSessions == -1) {// We permit unlimited loginsreturn;}if (sessionCount == allowedSessions) {HttpSession session = request.getSession(false);if (session != null) {// Only permit it though if this request is associated with one of the// already registered sessionsfor (SessionInformation si : sessions) {if (si.getSessionId().equals(session.getId())) {return;}}}// If the session is null, a new one will be created by the parent class,// exceeding the allowed number}allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry);}protected void allowableSessionsExceeded(List<SessionInformation> sessions,int allowableSessions, SessionRegistry registry)throws SessionAuthenticationException {if (exceptionIfMaximumExceeded || (sessions == null)) {throw new SessionAuthenticationException(messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",new Object[] {allowableSessions},"Maximum sessions of {0} for this principal exceeded"));}// Determine least recently used sessions, and mark them for invalidationsessions.sort(Comparator.comparing(SessionInformation::getLastRequest));int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);for (SessionInformation session: sessionsToBeExpired) {session.expireNow();}}
}

这段核心代码我来给大家稍微解释下:

  1. 首先调用 sessionRegistry.getAllSessions 方法获取当前用户的所有 session,该方法在调用时,传递两个参数,一个是当前用户的 authentication,另一个参数 false 表示不包含已经过期的 session(在用户登录成功后,会将用户的 sessionid 存起来,其中 key 是用户的主体(principal),value 则是该主题对应的 sessionid 组成的一个集合)。
  2. 接下来计算出当前用户已经有几个有效 session 了,同时获取允许的 session 并发数。
  3. 如果当前 session 数(sessionCount)小于 session 并发数(allowedSessions),则不做任何处理;如果 allowedSessions 的值为 -1,表示对 session 数量不做任何限制。
  4. 如果当前 session 数(sessionCount)等于 session 并发数(allowedSessions),那就先看看当前 session 是否不为 null,并且已经存在于 sessions 中了,如果已经存在了,那都是自家人,不做任何处理;如果当前 session 为 null,那么意味着将有一个新的 session 被创建出来,届时当前 session 数(sessionCount)就会超过 session 并发数(allowedSessions)。
  5. 如果前面的代码中都没能 return 掉,那么将进入策略判断方法 allowableSessionsExceeded 中。
  6. allowableSessionsExceeded 方法中,首先会有 exceptionIfMaximumExceeded 属性,这就是我们在 SecurityConfig 中配置的 maxSessionsPreventsLogin 的值,默认为 false,如果为 true,就直接抛出异常,那么这次登录就失败了(对应 2.2 小节的效果),如果为 false,则对 sessions 按照请求时间进行排序,然后再使多余的 session 过期即可(对应 2.1 小节的效果)。

4.小结

如此,两行简单的配置就实现了 Spring Security 中 session 的并发管理。是不是很简单?该项目源码及往期教程我已经放在码云上了,有需要的朋友可以下载看看https://gitee.com/wavefar/spring-security-samples,不过这里还有一个小小的坑,我们将在下篇文章中继续和大家分析。

【Spring Security】如何实现多设备同一时间只允许一个账号登录(即前登录用户被后登录用户挤下线)?只需简单两步!相关推荐

  1. 简单两步,spring aop上手即用即会

    面向切面思想在于它的干净,对逻辑代码没有任何侵入性,只需要在想要切入的方法或者类之上加上自定义的注解即可. 首先,就是自定义一个注解: //这里我们定义一个名为 @MyPointer 的注解 @Doc ...

  2. Spring Security 安全框架

    Spring Security 一. Spring Security 简介 1 概括 Spring Security 是一个高度自定义的安全框架.利用 Spring IoC/DI和 AOP 功能,为系 ...

  3. Spring Security 参考手册(一)

    Spring Security 参考手册 Ben AlexLuke TaylorRob WinchGunnar Hillert Spring security 是一个强大的和高度可定制的身份验证和访问 ...

  4. 大仙教学 Spring Security

    大仙花式讲解Spring Security 1. SpringSecurity 框架简介 1.1 概要 Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 ...

  5. 【Spring】Spring Security介绍及其入门案例

    文章目录 前言 1. SpringSecurity 框架简介 1.1 概要 1.2 历史 1.3 同款产品对比 1.3.1 Spring Security 1.3.2 Shiro 1.4 模块划分 2 ...

  6. Spring Security 5.0.x 参考手册 【翻译自官方GIT-2018.06.12】

    源码请移步至: https://github.com/aquariuspj/spring-security/tree/translator/docs/manual/src/docs/asciidoc ...

  7. SpringSecurity[1]-SpringSecurity简介以及创建Spring Security第一个项目

    主要内容 Spring Security 简介 第一个Spring Security项目 UserDetailsService详解 PasswordEncoder密码解析器详解 自定义登录逻辑 自定义 ...

  8. spring security remember me实现自动登录

    1 默认策略 在我们自定义的login中增加一个选择框 <input type="submit" value="Login" /> <br/& ...

  9. 9.Spring Security添加记住我功能

    在网站的登录页面中,记住我选项是一个很常见的功能,勾选记住我后在一段时间内,用户无需进行登录操作就可以访问系统资源.在Spring Security中添加记住我功能很简单,大致过程是:当用户勾选了记住 ...

最新文章

  1. 【OGG】OGG的单向复制配置-支持DDL(二)
  2. jpa mysql乐观锁_JPA @Lock(value = LockModeType.PESSIMISTIC_WRITE) 悲观锁防坑
  3. git入门:概念、原理、使用
  4. springMVC如何接收和发送json数据对象
  5. 透过性别看世界_透过树林看森林
  6. 清华大学《操作系统》(十一):处理机调度
  7. for循环中gets_Python中for循环的一些非常规操作
  8. Unity 检测物体是否在相机视野范围内
  9. 多目标数据关联基本方法
  10. linux服务器安装centos7,Linux服务器Centos7安装搭建FTP服务器的方法步骤
  11. mysql中grade字段降序排列_Mysql order by 多个字段排序
  12. ISDN-PRI,1号,7号信令的基础知识
  13. 花瓣网爬虫Python
  14. matlab ode45求解齿轮动力学,Matlab讨论区 - 声振论坛 - 振动,动力学,声学,信号处理,故障诊断 - Powered by Discuz!...
  15. 《昆虫记》思维导图|思维导图模板创意漂亮
  16. 怎么用静图做gif动图?三步教你轻松做动图
  17. 开源软件之screen的第一次使用
  18. 一图秒懂:打开oracle归档模式,rman备份的前提条件
  19. MultiDex精补篇,进一步知道MultiDex的配置
  20. 机器学习算法实践-SVM中的SMO算法

热门文章

  1. 举例说明一下常见的弱口令_常见网站入侵手段及防御方法
  2. 这个夏天不能错过的IT经典好书大盘点
  3. flex 左右 两边
  4. 用vue仿淘宝商品筛选
  5. 双核跟四核的区别linux,双核和四核有什么区别?教你区分双核和四核的方法
  6. Https数字证书交换过程介绍
  7. mysql 对分类汇总进行排序_如何对excel分类汇总的数据进行排序
  8. 中国到英国海运需要多长时间
  9. “父亲节”为程序员正名:谁说程序员不会表达爱?
  10. 数学题 识别 批改 python_这款软件能帮小学老师家长批作业 1秒扫描口算题圈错 准确率95%...