Springboot +spring security,实现session并发控制及实现原理分析

时间:2024-10-12 16:35:19

一.简介

在SpringSecurity中实现会话并发控制,只需要配置一个会话数量就可以了,先介绍下如何配置会话并发控制,然后再。介绍下SpringSecurity 如何实现会话并发控制。

二.创建项目

如何创建一个SpringSecurity项目,前面文章已经有说明了,这里就不重复写了。

三.代码实现

3.1设置只有一个会话

SecurityConfig 类,代码如下:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        ()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .sessionManagement()
                .maximumSessions(1);
        return ();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

登陆一个客户端

在这里插入图片描述
登陆第二个客户端
在这里插入图片描述
刷新第一个客户端
在这里插入图片描述
这时候发现已经被挤掉了。

目前默认策略是:后来的会把前面的给挤掉,现在我们通过配置,禁止第二个客户端登陆

3.2禁止第二个客户端登陆

SecurityConfig 类,代码如下:

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        ()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .sessionManagement()
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true);
        return ();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

登陆第一个客户端
在这里插入图片描述
登陆第二个客户端
在这里插入图片描述

四.实现原理分析

session默认的过滤器是SessionManagementConfigurer

4.1SessionManagementConfigurer

点击.sessionManagement()进去,找到SessionManagementConfigurer,点进去看下主要是看init和configure方法。

4.1.1 init()方法
  1. 创建SecurityContextRepository
  2. 初始化 SessionAuthenticationStrategy,并添加到容器
    ConcurrentSessionControlAuthenticationStrategy
    defaultSessionAuthenticationStrategy
    RegisterSessionAuthenticationStrategy
    setMaximumSessions
    setExceptionIfMaximumExceeded = maxSessionsPreventsLogin
    CompositeSessionAuthenticationStrategy
    InvalidSessionStrategy
4.1.2 configure()方法
  1. 初始化 SessionManagementFilter
  2. 添加sessionManagementFilter 到http链中
  3. isConcurrentSessionControlEnabled &&添加ConcurrentSessionFilter 到http链中
  4. ! && 添加 DisableEncodeUrlFilter
  5. && 添加 ForceEagerSessionCreationFilter

4.2CompositeSessionAuthenticationStrategy

CompositeSessionAuthenticationStrategy是一个代理策略,它里面会包含很多SessionAuthenticationStrategy,主要有ConcurrentSessionControlAuthenticationStrategy和RegisterSessionAuthenticationStrategy。

4.3RegisterSessionAuthenticationStrategy

处理并发登录人数的数量,代码如下:

 public void onAuthentication(Authentication authentication, HttpServletRequest request,
   HttpServletResponse response) {
  (().getId(), ());
 }
  • 1
  • 2
  • 3
  • 4

这里直接调用方法,代码如下:

public void registerNewSession(String sessionId, Object principal) {
  (sessionId, "SessionId required as per interface contract");
  (principal, "Principal required as per interface contract");
  if (getSessionInformation(sessionId) != null) {
   removeSessionInformation(sessionId);
  }
  (sessionId, new SessionInformation(principal, sessionId, new Date()));
  (principal, (key, sessionsUsedByPrincipal) -> {
   if (sessionsUsedByPrincipal == null) {
    sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
   }
   (sessionId);
   (("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
   return sessionsUsedByPrincipal;
  });
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  1. 根据sessionId查找session,如果有,则移除
  2. 创建新的SessionInformation,维护到sessionIds中
  3. 维护sessionId到principals中

4.4ConcurrentSessionControlAuthenticationStrategy

onAuthentication方法代码如下:

public void onAuthentication(Authentication authentication, HttpServletRequest request,
   HttpServletResponse response) {
  int allowedSessions = getMaximumSessionsForThisUser(authentication);
  if (allowedSessions == -1) {
   // We permit unlimited logins
   return;
  }
  List<SessionInformation> sessions = ((), false);
  int sessionCount = ();
  if (sessionCount < allowedSessions) {
   // They haven't got too many login sessions running at present
   return;
  }
  if (sessionCount == allowedSessions) {
   HttpSession session = (false);
   if (session != null) {
    // Only permit it though if this request is associated with one of the
    // already registered sessions
    for (SessionInformation si : sessions) {
     if (().equals(())) {
      return;
     }
    }
   }
   // If the session is null, a new one will be created by the parent class,
   // exceeding the allowed number
  }
  allowableSessionsExceeded(sessions, allowedSessions, );
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  1. 获取当前用户允许同时在线的数量,如果 == -1(没有限制)则跳过并发校验
  2. 获取当前用户的所有在线session数量,如果小于限制数量则返回
  3. 如果等于限制数量,则判断当前的sessionId是否已经在集合中,如果在,则返回
  4. 否则走allowableSessionsExceeded 校验

allowableSessionsExceeded方法代码如下:

 protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
   SessionRegistry registry) throws SessionAuthenticationException {
  if ( || (sessions == null)) {
   throw new SessionAuthenticationException(
     ("",
       new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
  }
  // Determine least recently used sessions, and mark them for invalidation
  ((SessionInformation::getLastRequest));
  int maximumSessionsExceededBy = () - allowableSessions + 1;
  List<SessionInformation> sessionsToBeExpired = (0, maximumSessionsExceededBy);
  for (SessionInformation session : sessionsToBeExpired) {
   ();
  }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  1. 如果配置了 maxSessionsPreventsLogin,则直接抛出异常,禁止新用户登录,否则往下走
  2. 将当前用户的所有session按照最后访问时间排序
  3. 获取最大允许同时在线的数量,然后在集合中 top n,其余的全部设置过期
  4. expireNow();
    = true;

4.5SessionManagementFilter

代码如下:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
   throws IOException, ServletException {
  if (!(request)) {
   Authentication authentication = ().getAuthentication();
   if (authentication != null && !(authentication)) {
    // The user has been authenticated during the current request, so call the
    // session strategy
    try {
     (authentication, request, response);
    }
    catch (SessionAuthenticationException ex) {
     ();
     (request, response, ex);
     return;
    }    ((), request, response);
   }
   else {
    if (() != null && !()) {
     if ( != null) {
     (request, response);
      return;
     }
    }
   }
  }
  (request, response);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  1. 如果securityContextRepository 有Context信息
  2. 如果抛出异常,则进行异常处理,并清楚context信息
    获取authentication
    如果authentication 不为空,则调用
    如果为空,则调用invalidSessionStrategy的onInvalidSessionDetected方法

4.6ConcurrentSessionFilter

ConcurrentSessionFilter类,代码如下:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
   throws IOException, ServletException {
  HttpSession session = (false);
  if (session != null) {
   SessionInformation info = (());
   if (info != null) {
    if (()) {
     // Expired - abort processing
     (LogMessage
       .of(() -> "Requested session ID " + () + " has expired."));
     doLogout(request, response);
     
       .onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
     return;
    }
    // Non-expired - update last request date/time
    (());
   }
  }
  (request, response);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

4.7AbstractAuthenticationProcessingFilter

这个过滤器也会调用,进行session维护,代码如下:

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
   throws IOException, ServletException {
  
   Authentication authenticationResult = attemptAuthentication(request, response);
   (authenticationResult, request, response);
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

整体流程图,截图如下:
在这里插入图片描述