SpringSecurity系列之只允许一台设备在线

架构 2023-07-05 17:29:38
51阅读

登录成功后,全自动踢出去前一个登陆客户,松哥第一次看到这一作用,便是在QQ里面看到的,那时候感觉太好玩了的。

自己做开发设计后,也遇到过一模一样的要求,恰好近期的 Spring Security 系列产品已经更新连载,就融合 Spring Security 来和大伙儿聊一聊这一作用怎样完成。

1.需求分析报告

在同一个系统软件中,大家很有可能只容许一个客户在一个终端设备上登陆,一般来说这可能是出自于安全性层面的考虑到,可是也是有一些状况是出自于业务流程上的考虑到,松哥以前碰到的要求便是业务流程缘故规定一个客户只有在一个机器设备上登陆。

要完成一个客户不能另外在两部机器设备上登陆,大家有二种构思:

之后的登陆全自动踢出去前边的登陆,如同大伙儿在QQ中见到的实际效果。

假如客户早已登陆,则不允许幸不辱命登陆。

这类构思都能完成这一作用,实际应用哪一个,也要看大家实际的要求。

在 Spring Security 中,这二种都很好完成,一个配备就可以拿下。

2.实际完成

2.1 踢出去早已登陆客户

要想用新的登陆踢出去旧的登陆,大家只必须将较大 对话数设定为 1 就可以,配备以下:

 
  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests() 
  4.             .anyRequest().authenticated() 
  5.             .and() 
  6.             .formLogin() 
  7.             .loginPage("/login.html"
  8.             .permitAll() 
  9.             .and() 
  10.             .csrf().disable() 
  11.             .sessionManagement() 
  12.             .maximumSessions(1); 

maximumSessions 表明配备较大 对话数为 1,那样后边的登陆便会全自动踢出去前边的登陆。这儿别的的配备全是大家前边文章内容讲过的,我也不会再反复详细介绍,文尾可以下载实例详细编码。

配备进行后,各自用 Chrome 和 Firefox 2个电脑浏览器开展检测(或是应用 Chrome 中的多客户作用)。

  1. Chrome 上登录成功后,浏览 /hello 插口。
  2. Firefox 上登录成功后,浏览 /hello 插口。
  3. 在 Chrome 上再度浏览 /hello 插口,这时会见到以下提醒:
 
  1. This session has been expired (possibly due to multiple concurrent logins being attempted as the same user). 

能够见到,这儿说这一 session 早已到期,缘故则是因为应用同一个客户开展高并发登陆。

2.2 严禁新的登陆

假如同样的客户早已登陆了,你不想踢出去他,只是想严禁新的登陆实际操作,那也好办,配备方法以下:

 
  1. @Override 
  2. protected void configure(HttpSecurity http) throws Exception { 
  3.     http.authorizeRequests() 
  4.             .anyRequest().authenticated() 
  5.             .and() 
  6.             .formLogin() 
  7.             .loginPage("/login.html"
  8.             .permitAll() 
  9.             .and() 
  10.             .csrf().disable() 
  11.             .sessionManagement() 
  12.             .maximumSessions(1) 
  13.             .maxSessionsPreventsLogin(true); 

加上 maxSessionsPreventsLogin 配备就可以。这时一个浏览器登录取得成功后,此外一个电脑浏览器就登陆不了。

是否非常简单?

但是还不停,大家还必须再出示一个 Bean:

 
  1. @Bean 
  2. HttpSessionEventPublisher httpSessionEventPublisher() { 
  3.     return new HttpSessionEventPublisher(); 

为何要加这一 Bean 呢?由于在 Spring Security 中,它是根据监视 session 的消毁事情,来立即的清除 session 的纪录。客户从不一样的浏览器登录后,都是会有相匹配的 session,当客户销户登陆以后,session 便会无效,可是默认设置的无效是根据启用 StandardSession#invalidate 方式来完成的,这一个无效事情没法被 Spring 器皿认知到,从而造成当客户销户登陆以后,Spring Security 沒有立即清除对话备案表,认为客户还线上,从而导致用户没法再次登陆进去(朋友们能够自主试着不加上上边的 Bean,随后让客户销户登陆以后再再次登陆)。

为了更好地处理这一难题,大家出示一个 HttpSessionEventPublisher ,这一类完成了 HttpSessionListener 插口,在该 Bean 中,能够将 session 建立及其消毁的事情立即认知到,而且启用 Spring 中的事情体制将有关的建立和消毁事情公布出来 ,从而被 Spring Security 认知到,此类一部分源代码以下:

 
  1. public void sessionCreated(HttpSessionEvent event) { 
  2.  HttpSessionCreatedEvent e = new HttpSessionCreatedEvent(event.getSession()); 
  3.  getContext(event.getSession().getServletContext()).publishEvent(e); 
  4. public void sessionDestroyed(HttpSessionEvent event) { 
  5.  HttpSessionDestroyedEvent e = new HttpSessionDestroyedEvent(event.getSession()); 
  6.  getContext(event.getSession().getServletContext()).publishEvent(e); 

OK,尽管多了一个配备,可是仍然非常简单!

3.完成基本原理

上边这一作用,在 Spring Security 中是怎么完成的呢?大家来略微剖析一下源代码。

最先我们知道,在账号登录的全过程中,会历经 UsernamePasswordAuthenticationFilter,而 UsernamePasswordAuthenticationFilter 中过虑方式的启用是在 AbstractAuthenticationProcessingFilter 中开启的,大家看来下 AbstractAuthenticationProcessingFilter#doFilter 方式的启用:

 
  1. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
  2.   throws IOException, ServletException { 
  3.  HttpServletRequest request = (HttpServletRequest) req; 
  4.  HttpServletResponse response = (HttpServletResponse) res; 
  5.  if (!requiresAuthentication(request, response)) { 
  6.   chain.doFilter(request, response); 
  7.   return
  8.  } 
  9.  Authentication authResult; 
  10.  try { 
  11.   authResult = attemptAuthentication(request, response); 
  12.   if (authResult == null) { 
  13.    return
  14.   } 
  15.   sessionStrategy.onAuthentication(authResult, request, response); 
  16.  } 
  17.  catch (InternalAuthenticationServiceException failed) { 
  18.   unsuccessfulAuthentication(request, response, failed); 
  19.   return
  20.  } 
  21.  catch (AuthenticationException failed) { 
  22.   unsuccessfulAuthentication(request, response, failed); 
  23.   return
  24.  } 
  25.  // Authentication success 
  26.  if (continueChainBeforeSuccessfulAuthentication) { 
  27.   chain.doFilter(request, response); 
  28.  } 
  29.  successfulAuthentication(request, response, chain, authResult); 

在这里段编码中,我们可以见到,启用 attemptAuthentication 方式走完验证步骤以后,回家以后,下面便是启用 sessionStrategy.onAuthentication 方式,这一方式便是用于解决 session 的高并发难题的。实际在:

 
  1. public class ConcurrentSessionControlAuthenticationStrategy implements 
  2.   MessageSourceAware, SessionAuthenticationStrategy { 
  3.  public void onAuthentication(Authentication authentication, 
  4.    HttpServletRequest request, HttpServletResponse response) { 
  5.  
  6.   final List<SessionInformation> sessions = sessionRegistry.getAllSessions( 
  7.     authentication.getPrincipal(), false); 
  8.  
  9.   int sessionCount = sessions.size(); 
  10.   int allowedSessions = getMaximumSessionsForThisUser(authentication); 
  11.  
  12.   if (sessionCount < allowedSessions) { 
  13.    // They haven't got too many login sessions running at present 
  14.    return
  15.   } 
  16.  
  17.   if (allowedSessions == -1) { 
  18.    // We permit unlimited logins 
  19.    return
  20.   } 
  21.  
  22.   if (sessionCount == allowedSessions) { 
  23.    HttpSession session = request.getSession(false); 
  24.  
  25.    if (session != null) { 
  26.     // Only permit it though if this request is associated with one of the 
  27.     // already registered sessions 
  28.     for (SessionInformation si : sessions) { 
  29.      if (si.getSessionId().equals(session.getId())) { 
  30.       return
  31.      } 
  32.     } 
  33.    } 
  34.    // If the session is null, a new one will be created by the parent class, 
  35.    // exceeding the allowed number 
  36.   } 
  37.  
  38.   allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); 
  39.  } 
  40.  protected void allowableSessionsExceeded(List<SessionInformation> sessions, 
  41.    int allowableSessions, SessionRegistry registry) 
  42.    throws SessionAuthenticationException { 
  43.   if (exceptionIfMaximumExceeded || (sessions == null)) { 
  44.    throw new SessionAuthenticationException(messages.getMessage( 
  45.      "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed"
  46.      new Object[] {allowableSessions}, 
  47.      "Maximum sessions of {0} for this principal exceeded")); 
  48.   } 
  49.  
  50.   // Determine least recently used sessions, and mark them for invalidation 
  51.   sessions.sort(Comparator.comparing(SessionInformation::getLastRequest)); 
  52.   int maximumSessionsExceededBy = sessions.size() - allowableSessions   1; 
  53.   List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy); 
  54.   for (SessionInformation session: sessionsToBeExpired) { 
  55.    session.expireNow(); 
  56.   } 
  57.  } 

这一段关键编码我给大伙儿略微表述下:

  1. 最先启用 sessionRegistry.getAllSessions 方式获得当今客户的全部 session,该方式在启用时,传送2个主要参数,一个是当今客户的 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 的高并发管理方法。是否非常简单?但是这儿还有一个小小坑,松哥将在下一篇文章中再次和大伙儿剖析。

文中实例大伙儿能够从 GitHub 上免费下载:https://github.com/lenve/spring-security-samples

the end
免责声明:本文不代表本站的观点和立场,如有侵权请联系本站删除!本站仅提供信息存储空间服务。