,登录成功后自动踢出上次登录用户。松哥第一次看到这个功能是在口口,当时觉得挺好玩的。自己做开发后,也遇到了完全一样的需求。正好最新的SpringSecurity系列正在连载中,下面就和大家谈谈如何结合SpringSecurity实现这个功能。1、需求分析在同一个系统中,我们可能在一个终端上只允许一个用户登录。一般来说,这可能是出于安全考虑,但也有一些情况是出于业务考虑。松哥之前遇到的需求是业务原因要求一个用户只能在一台设备上登录。实现一个用户不能同时登录两台设备,我们有两个思路:后面的登录自动踢出前面的登录,就像QUOUQUAN中看到的效果。如果用户已经登录,则不允许以后登录。这个思路可以实现这个功能,具体使用哪一个就看我们的具体需求了。在SpringSecurity中,这两种实现起来都很容易,一次配置即可。2、具体实现2.1踢掉已登录用户如果想用新登录踢掉旧登录,我们只需要将最大会话数设置为1即可。配置如下:@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.authorizeRequests().anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1);}maximumSessions表示配置的最大会话数为1,这样以后的登录会自动踢掉之前的登录。这里的其他配置在我们之前的文章中都有提到,这里不再重复介绍。案例完整代码可在文末下载。配置完成后,使用Chrome和Firefox进行测试(或者使用Chrome中的多用户功能)。Chrome登录成功后,访问/hello界面。在Firefox上登录成功后,访问/hello界面。在Chrome上再次访问/hello界面,会看到如下提示:Thissessionhasbeenexpired(possiblyduetommultipleconcurrentloginsbeingattemptedasthesameuser)。您可以看到会话已过期,因为同一用户用于并发登录。2.2禁止新登录如果同一个用户已经登录了,你不想把他踢出去,而是想禁止新的登录操作,这很容易做到。配置方法如下:@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{http.authorizeRequests()。anyRequest().authenticated().and().formLogin().loginPage("/login.html").permitAll().and().csrf().disable().sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);}只需添加maxSessionsPreventsLogin配置。这时一个浏览器登录成功后,另一个浏览器无法登录,是不是很简单?但是还没完,我们还需要再提供一个Bean:@BeanHttpSessionEventPublisherhttpSessionEventPublisher(){returnnewHttpSessionEventPublisher();}为什么要添加这个Bean呢?因为在SpringSecurity中,它会监听session的销毁事件,及时清理session记录。用户从不同的浏览器登录后,都会有相应的session。当用户注销再登录时,session会失效,但默认失效是通过调用StandardSession#invalidate方法实现的。这个失效事件无法被Spring容器感知到,导致用户注销再登录时,SpringSecurity没有及时清理session信息表,认为用户还在线,导致用户无法重新登录(可以尽量不要自己添加上面的Bean,然后让用户注销重新登录再登录)。为了解决这个问题,我们提供了一个HttpSessionEventPublisher,它实现了HttpSessionListener接口。在这个Bean中,可以及时感知session的创建和销毁事件,调用Spring中的事件机制发布相关的创建和销毁事件。出去被SpringSecurity感知。该类源码如下:){HttpSessionDestroyedEvente=newHttpSessionDestroyedEvent(event.getSession());getContext(event.getSession().getServletContext()).publishEvent(e);}OK,虽然多了一个配置,但是还是很简单的!3.实现原理上面的功能在SpringSecurity中是如何实现的?让我们稍微分析一下源代码。首先,我们知道在用户登录的过程中,UsernamePasswordAuthenticationFilter会经过,在AbstractAuthenticationProcessingFilter中触发UsernamePasswordAuthenticationFilter中filter方法的调用,我们看AbstractAuthenticationProcessingFilter#doFilterIO方法的调用:publicvoiddoFilter(ServletRequestreq,ServletFilthResponseres)ServletException{HttpServletRequestrequest=(HttpServletRequest)req;HttpServletResponseresponse=(HttpServletResponse)res;if(!requiresAuthentication(request,response)){chain.doFilter(request,response);return;}AuthenticationauthResult;try{authResult=att请求、响应);如果(authResult==null){返回;}sessionStrategy.onAuthentication(authResult、请求、响应);}catch(InternalAuthenticationServiceExceptionfailed){unsuccessfulAuthentication(请求、响应、失败);返回;}catch(AuthenticationExceptionrequestfailed){Authentication(,response,failed);return;}//认证成功if(continueChainBeforeSuccessfulAuthentication){chain.doFilter(request,response);}successfulAuthentication(request,response,chain,authResult);在这段代码中,我们可以看到在调用attemptAuthentication方法完成认证过程后,返回后,下一步就是调用sessionStrategy.onAuthentication方法,用于处理session并发问题具体在:publicclassConcurrentSessionControlAuthenticationStrategyimplementsMessageSourceAware,SessionAuthenticationStrategy{publicvoidonAuthentication(Authenticationauthentication,HttpServletRequestrequest,HttpServletResponseresponse){finalList
