会话管理

当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话Session,浏览器在每次发送请求时都会携带一个SessionId,服务器会根据这个SessionId来判断用户身份。当浏览器关闭后,服务端的Session并不会自动销毁,需要开发者手动在服务端调用Session销毁方法,或者等Session过期时间到了自动销毁。

会话并发管理就是指在当前系统中,同一个用户可以同时创建多少个会话,如果一台设备对应一个会话,那么可以简单理解为同一个用户可以同时在多少台设备上登录,默认同一个用户在设备上登录并没有限制,可以在Security中配置。

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .and()
            .csrf()
            .disable()
            .sessionManagement()
            .sessionFixation()
            .none()
            .maximumSessions(1)
            .expiredSessionStrategy(event -> {
                HttpServletResponse response = event.getResponse();
                response.setContentType("application/json;charset=utf-8");
                Map<String, Object> result = new HashMap<>();
                result.put("status", 500);
                result.put("msg", "当前会话已经失效,请重新登录");
                String s = new ObjectMapper().writeValueAsString(result);
                response.getWriter().print(s);
                response.flushBuffer();
            });
}

在登录过滤器AbstractAuthenticationProcessingFilter的doFilter方法中,调用attemptAuthentication方法进行登录认证后,调用sessionStrategy.onAuthentication方法进行Session并发的管理,默认是CompositeSessionAuthenticationStrategy

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;
   }

   if (logger.isDebugEnabled()) {
      logger.debug("Request is to process authentication");
   }

   Authentication authResult;

   try {
      authResult = attemptAuthentication(request, response);
      if (authResult == null) {
         // return immediately as subclass has indicated that it hasn't completed
         // authentication
         return;
      }
      sessionStrategy.onAuthentication(authResult, request, response);
   }
   catch (InternalAuthenticationServiceException failed) {
      logger.error(
            "An internal error occurred while trying to authenticate the user.",
            failed);
      unsuccessfulAuthentication(request, response, failed);

      return;
   }
   catch (AuthenticationException failed) {
      // Authentication failed
      unsuccessfulAuthentication(request, response, failed);

      return;
   }

   // Authentication success
   if (continueChainBeforeSuccessfulAuthentication) {
      chain.doFilter(request, response);
   }

   successfulAuthentication(request, response, chain, authResult);
}

CompositeSessionAuthenticationStrategy的onAuthentication方法中遍历集合,依次调用集合元素的onAuthentication方法

public void onAuthentication(Authentication authentication,
      HttpServletRequest request, HttpServletResponse response)
            throws SessionAuthenticationException {
   for (SessionAuthenticationStrategy delegate : this.delegateStrategies) {
      if (this.logger.isDebugEnabled()) {
         this.logger.debug("Delegating to " + delegate);
      }
      delegate.onAuthentication(authentication, request, response);
   }
}

sessionStrategy是AbstractAuthenticationFilterConfigurer类的configure方法中进行配置的,可以看到,这里从HttpSecurity的共享对象中获取到SessionAuthenticationStrategy的实例,并设置到authFilter过滤器中

public void configure(B http) throws Exception {
   PortMapper portMapper = http.getSharedObject(PortMapper.class);
   if (portMapper != null) {
      authenticationEntryPoint.setPortMapper(portMapper);
   }

   RequestCache requestCache = http.getSharedObject(RequestCache.class);
   if (requestCache != null) {
      this.defaultSuccessHandler.setRequestCache(requestCache);
   }

   authFilter.setAuthenticationManager(http
         .getSharedObject(AuthenticationManager.class));
   authFilter.setAuthenticationSuccessHandler(successHandler);
   authFilter.setAuthenticationFailureHandler(failureHandler);
   if (authenticationDetailsSource != null) {
      authFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
   }
   SessionAuthenticationStrategy sessionAuthenticationStrategy = http
         .getSharedObject(SessionAuthenticationStrategy.class);
   if (sessionAuthenticationStrategy != null) {
      authFilter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
   }
   RememberMeServices rememberMeServices = http
         .getSharedObject(RememberMeServices.class);
   if (rememberMeServices != null) {
      authFilter.setRememberMeServices(rememberMeServices);
   }
   F filter = postProcess(authFilter);
   http.addFilter(filter);
}

SessionAuthenticationStrategy的实例是在SessionManagementConfigurer的init方法中存入的

public void init(H http) {
   SecurityContextRepository securityContextRepository = http
         .getSharedObject(SecurityContextRepository.class);
   boolean stateless = isStateless();

   if (securityContextRepository == null) {
      if (stateless) {
         http.setSharedObject(SecurityContextRepository.class,
               new NullSecurityContextRepository());
      }
      else {
         HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
         httpSecurityRepository
               .setDisableUrlRewriting(!this.enableSessionUrlRewriting);
         httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
         AuthenticationTrustResolver trustResolver = http
               .getSharedObject(AuthenticationTrustResolver.class);
         if (trustResolver != null) {
            httpSecurityRepository.setTrustResolver(trustResolver);
         }
         http.setSharedObject(SecurityContextRepository.class,
               httpSecurityRepository);
      }
   }

   RequestCache requestCache = http.getSharedObject(RequestCache.class);
   if (requestCache == null) {
      if (stateless) {
         http.setSharedObject(RequestCache.class, new NullRequestCache());
      }
   }
   http.setSharedObject(SessionAuthenticationStrategy.class,
         getSessionAuthenticationStrategy(http));
   http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
}

方法中 首先从HttpSecurity中获取SecurityContextRepository实例,没有则进行创建,创建的时候如果是Session的创建策略是STATELESS,则使用NullSecurityContextRepository来保存SecurityContext,如果不是则构建HttpSessionSecurityContextRepository,并存入HTTPSecurity共享对象中。

如果Session的创建策略是STATELESS,还要把请求缓存对象替换为NullRequestCache

最后构建SessionAuthenticationStrategy的实例和InvalidSessionStrategy的实例,SessionAuthenticationStrategy的实例从getSessionAuthenticationStrategy中获得

private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
   if (this.sessionAuthenticationStrategy != null) {
      return this.sessionAuthenticationStrategy;
   }
   List<SessionAuthenticationStrategy> delegateStrategies = this.sessionAuthenticationStrategies;
   SessionAuthenticationStrategy defaultSessionAuthenticationStrategy;
   if (this.providedSessionAuthenticationStrategy == null) {
      // If the user did not provide a SessionAuthenticationStrategy
      // then default to sessionFixationAuthenticationStrategy
      defaultSessionAuthenticationStrategy = postProcess(
            this.sessionFixationAuthenticationStrategy);
   }
   else {
      defaultSessionAuthenticationStrategy = this.providedSessionAuthenticationStrategy;
   }
   if (isConcurrentSessionControlEnabled()) {
      SessionRegistry sessionRegistry = getSessionRegistry(http);
      ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
            sessionRegistry);
      concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
      concurrentSessionControlStrategy
            .setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
      concurrentSessionControlStrategy = postProcess(
            concurrentSessionControlStrategy);

      RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
            sessionRegistry);
      registerSessionStrategy = postProcess(registerSessionStrategy);

      delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,
            defaultSessionAuthenticationStrategy, registerSessionStrategy));
   }
   else {
      delegateStrategies.add(defaultSessionAuthenticationStrategy);
   }
   this.sessionAuthenticationStrategy = postProcess(
         new CompositeSessionAuthenticationStrategy(delegateStrategies));
   return this.sessionAuthenticationStrategy;
}

getSessionAuthenticationStrategy方法中把ConcurrentSessionControlAuthenticationStrategy ChangeSessionIdAuthenticationStrategy RegisterSessionAuthenticationStrategy添加到集合中,并返回代理类CompositeSessionAuthenticationStrategy

而sessionStrategy

ConcurrentSessionControlAuthenticationStrategy

主要用来处理Session并发问题,并发控制实际是由这个类来完成的

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 present
      return;
   }

   if (allowedSessions == -1) {
      // We permit unlimited logins
      return;
   }

   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 sessions
         for (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);
}
  1. 从sessionRegistry中获取当前用户所有未失效的SessionInformation,然后获取当前项目允许的最大session数。如果获取到的SessionInformation实例小于当前项目允许的最大session数,说明当前登录没有问题,直接return
  2. 如果允许的最大session数为-1,表示应用并不限制登录并发数,当前登录没有问题,直接return
  3. 如果两者相等,判断当前sessionId是否在SessionInformation中,如果存在,直接return
  4. 超出最大并发数,进入allowableSessionsExceeded方法
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 invalidation
   sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
   int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
   List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
   for (SessionInformation session: sessionsToBeExpired) {
      session.expireNow();
   }
}

allowableSessionsExceeded方法中判断exceptionIfMaximumExceeded属性为true,则直接抛出异常,exceptionIfMaximumExceeded的属性是在SecurityConfig中

通过maxSessionPreventsLogin方法的值来改变,即禁止后来者的登录,抛出异常后,本次登录失败。否则对查询当前用户所有登录的session按照最后一次请求时间进行排序,计算出需要过期的session数量,从session集合中取出来进行遍历,依次调用expireNow方法让session过期。

ChangeSessionIdAuthenticationStrategy

通过修改sessionId来防止会话固定攻击。

所谓会话固定攻击是一种潜在的风险,恶意攻击者可能通过访问当前应用程序来创建会话,然后诱导用户以相同的会话Id登录,进而获取用户登录身份。

RegisterSessionAuthenticationStrategy

在认证成功后把HttpSession信息记录到SessionRegistry中。

public void onAuthentication(Authentication authentication,
      HttpServletRequest request, HttpServletResponse response) {
   sessionRegistry.registerNewSession(request.getSession().getId(),
         authentication.getPrincipal());
}

用户使用RememberMe的方式进行身份认证,则会通过SessionManagementFilter的doFilter方法触发Session并发管理。

SessionManagementConfigurer的configure方法中构建了这两个过滤器SessionManagementFilter和ConcurrentSessionFilter

public void configure(H http) {
   SecurityContextRepository securityContextRepository = http
         .getSharedObject(SecurityContextRepository.class);
   SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(
         securityContextRepository, getSessionAuthenticationStrategy(http));
   if (this.sessionAuthenticationErrorUrl != null) {
      sessionManagementFilter.setAuthenticationFailureHandler(
            new SimpleUrlAuthenticationFailureHandler(
                  this.sessionAuthenticationErrorUrl));
   }
   InvalidSessionStrategy strategy = getInvalidSessionStrategy();
   if (strategy != null) {
      sessionManagementFilter.setInvalidSessionStrategy(strategy);
   }
   AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
   if (failureHandler != null) {
      sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
   }
   AuthenticationTrustResolver trustResolver = http
         .getSharedObject(AuthenticationTrustResolver.class);
   if (trustResolver != null) {
      sessionManagementFilter.setTrustResolver(trustResolver);
   }
   sessionManagementFilter = postProcess(sessionManagementFilter);

   http.addFilter(sessionManagementFilter);
   if (isConcurrentSessionControlEnabled()) {
      ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);

      concurrentSessionFilter = postProcess(concurrentSessionFilter);
      http.addFilter(concurrentSessionFilter);
   }
}
  1. SessionManagementFilter创建过程中调用getSessionAuthenticationStrategy方法获取SessionAuthenticationStrategy的实例放入过滤器中,然后配置各种回调函数,最终创建的SessionManagementFilter过滤器放入HttpSecurity中。
  2. 如果开启会话并发控制(只要maximumSessions不会空就算开启会话并发控制),则创建ConcurrentSessionFilter过滤器 加入到HttpSecurity中。

总结

用户通过用户名密码发起认证请求,当认证成功后,在AbstractAuthenticationProcessingFilter的doFilter方法中触发Session并发管理。默认的sessionStrategy是CompositeSessionAuthenticationStrategy,它代理了三个类ConcurrentSessionControlAuthenticationStrategy ChangeSessionIdAuthenticationStrategy RegisterSessionAuthenticationStrategy。当前请求在这三个SessionAuthenticationStrategy中分别走一圈,第一个用来判断当前用户的Session数是否超过限制,第二个用来修改sessionId(防止会话固定攻击),第三个用来将当前Session注册到SessionRegistry中。

如果用户使用RememberMe的方式进行身份认证,则会通过SessionManagementFilter的doFilter方法触发Session并发管理。当用户认证成功后,以后的每一次请求都会经过ConcurrentSessionFilter,在该过滤器中,判断当前会话是否过期,如果过期执行注销流程,如果没有过期,更新最近一次请求时间。