【Spring Security】如何实现多设备同一时间只允许一个账号登录(即前登录用户被后登录用户挤下线)?只需简单两步!
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 中隐私模式登录模拟多用户功能)。
- Chrome 上登录成功后,访问 /hello 接口。
- 利用 Chrome隐私模式模式上登录成功后,访问 /hello 接口。
- 在 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();}}
}
这段核心代码我来给大家稍微解释下:
- 首先调用 sessionRegistry.getAllSessions 方法获取当前用户的所有 session,该方法在调用时,传递两个参数,一个是当前用户的 authentication,另一个参数 false 表示不包含已经过期的 session(在用户登录成功后,会将用户的 sessionid 存起来,其中 key 是用户的主体(principal),value 则是该主题对应的 sessionid 组成的一个集合)。
- 接下来计算出当前用户已经有几个有效 session 了,同时获取允许的 session 并发数。
- 如果当前 session 数(sessionCount)小于 session 并发数(allowedSessions),则不做任何处理;如果 allowedSessions 的值为 -1,表示对 session 数量不做任何限制。
- 如果当前 session 数(sessionCount)等于 session 并发数(allowedSessions),那就先看看当前 session 是否不为 null,并且已经存在于 sessions 中了,如果已经存在了,那都是自家人,不做任何处理;如果当前 session 为 null,那么意味着将有一个新的 session 被创建出来,届时当前 session 数(sessionCount)就会超过 session 并发数(allowedSessions)。
- 如果前面的代码中都没能 return 掉,那么将进入策略判断方法 allowableSessionsExceeded 中。
- 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】如何实现多设备同一时间只允许一个账号登录(即前登录用户被后登录用户挤下线)?只需简单两步!相关推荐
- 简单两步,spring aop上手即用即会
面向切面思想在于它的干净,对逻辑代码没有任何侵入性,只需要在想要切入的方法或者类之上加上自定义的注解即可. 首先,就是自定义一个注解: //这里我们定义一个名为 @MyPointer 的注解 @Doc ...
- Spring Security 安全框架
Spring Security 一. Spring Security 简介 1 概括 Spring Security 是一个高度自定义的安全框架.利用 Spring IoC/DI和 AOP 功能,为系 ...
- Spring Security 参考手册(一)
Spring Security 参考手册 Ben AlexLuke TaylorRob WinchGunnar Hillert Spring security 是一个强大的和高度可定制的身份验证和访问 ...
- 大仙教学 Spring Security
大仙花式讲解Spring Security 1. SpringSecurity 框架简介 1.1 概要 Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 ...
- 【Spring】Spring Security介绍及其入门案例
文章目录 前言 1. SpringSecurity 框架简介 1.1 概要 1.2 历史 1.3 同款产品对比 1.3.1 Spring Security 1.3.2 Shiro 1.4 模块划分 2 ...
- Spring Security 5.0.x 参考手册 【翻译自官方GIT-2018.06.12】
源码请移步至: https://github.com/aquariuspj/spring-security/tree/translator/docs/manual/src/docs/asciidoc ...
- SpringSecurity[1]-SpringSecurity简介以及创建Spring Security第一个项目
主要内容 Spring Security 简介 第一个Spring Security项目 UserDetailsService详解 PasswordEncoder密码解析器详解 自定义登录逻辑 自定义 ...
- spring security remember me实现自动登录
1 默认策略 在我们自定义的login中增加一个选择框 <input type="submit" value="Login" /> <br/& ...
- 9.Spring Security添加记住我功能
在网站的登录页面中,记住我选项是一个很常见的功能,勾选记住我后在一段时间内,用户无需进行登录操作就可以访问系统资源.在Spring Security中添加记住我功能很简单,大致过程是:当用户勾选了记住 ...
最新文章
- 【OGG】OGG的单向复制配置-支持DDL(二)
- jpa mysql乐观锁_JPA @Lock(value = LockModeType.PESSIMISTIC_WRITE) 悲观锁防坑
- git入门:概念、原理、使用
- springMVC如何接收和发送json数据对象
- 透过性别看世界_透过树林看森林
- 清华大学《操作系统》(十一):处理机调度
- for循环中gets_Python中for循环的一些非常规操作
- Unity 检测物体是否在相机视野范围内
- 多目标数据关联基本方法
- linux服务器安装centos7,Linux服务器Centos7安装搭建FTP服务器的方法步骤
- mysql中grade字段降序排列_Mysql order by 多个字段排序
- ISDN-PRI,1号,7号信令的基础知识
- 花瓣网爬虫Python
- matlab ode45求解齿轮动力学,Matlab讨论区 - 声振论坛 - 振动,动力学,声学,信号处理,故障诊断 - Powered by Discuz!...
- 《昆虫记》思维导图|思维导图模板创意漂亮
- 怎么用静图做gif动图?三步教你轻松做动图
- 开源软件之screen的第一次使用
- 一图秒懂:打开oracle归档模式,rman备份的前提条件
- MultiDex精补篇,进一步知道MultiDex的配置
- 机器学习算法实践-SVM中的SMO算法
热门文章
- 举例说明一下常见的弱口令_常见网站入侵手段及防御方法
- 这个夏天不能错过的IT经典好书大盘点
- flex 左右 两边
- 用vue仿淘宝商品筛选
- 双核跟四核的区别linux,双核和四核有什么区别?教你区分双核和四核的方法
- Https数字证书交换过程介绍
- mysql 对分类汇总进行排序_如何对excel分类汇总的数据进行排序
- 中国到英国海运需要多长时间
- “父亲节”为程序员正名:谁说程序员不会表达爱?
- 数学题 识别 批改 python_这款软件能帮小学老师家长批作业 1秒扫描口算题圈错 准确率95%...