[SpringSecurity5.6.2源码分析十七]:SessionManagementConfigurer

217 阅读11分钟

前言

  • SessionManagementConfigurer是SpringSecurity中比较特殊的配置类了,他会注册两个过滤器
    • SessionManagementFilter:检测身份认证
    • ConcurrentSessionFilter:限制并发会话过滤器
  • 也是默认注册的

image.png

1. SessionManagementConfigurer

  • SessionManagementConfigurer的很多方法都是对于下面的这些属性进行配置
    /**
     * 会话固定身份验证策略
     */
    private final SessionAuthenticationStrategy DEFAULT_SESSION_FIXATION_STRATEGY = createDefaultSessionFixationProtectionStrategy();

    /**
     * 默认的HttpSession认证策略
     * 实际上也由public的set方法可以改变
     */
    private SessionAuthenticationStrategy sessionFixationAuthenticationStrategy = this.DEFAULT_SESSION_FIXATION_STRATEGY;

    /**
     * 最后将各个地方的HttpSession证策略组合在一起的 session认证策略
     * 是{@link CompositeSessionAuthenticationStrategy}
     */
    private SessionAuthenticationStrategy sessionAuthenticationStrategy;

    /**
     * 这个和下面这个都有Public的set方法,是让用户设置HttpSession认证策略
     * 但是这个和sessionFixationAuthenticationStrategy有冲突
     */
    private SessionAuthenticationStrategy providedSessionAuthenticationStrategy;

    private List<SessionAuthenticationStrategy> sessionAuthenticationStrategies = new ArrayList<>();

    /**
     * HttpSession过期(无效)策略
     */
    private InvalidSessionStrategy invalidSessionStrategy;

    /**
     * HttpSession过期(无效)之后跳转的Url
     * 就是创建一个跳转的策略
     */
    private String invalidSessionUrl;

    /**
     * SessionInformation过期策略
     */
    private SessionInformationExpiredStrategy expiredSessionStrategy;

    /**
     * SessionInformation过期之后跳转的Url
     * 就是创建一个跳转的策略
     */
    private String expiredUrl;

    /**
     * SessionInformation注册中心
     */
    private SessionRegistry sessionRegistry;

    /**
     * 限制用户会话并发数
     */
    private Integer maximumSessions;

    /**
     * 某个用户的会话数达到maximumSessions的时候,是否阻止登录
     * <ul>
     *     <li>
     *           true: 后面登录的用户直接抛出异常
     *     </li>
     *     <li>
     *       false:将最先登录的那个会话对应的SessionInformation直接设置为已过期,那么遇到ConcurrentSessionFilter就会有对应的退出操作了
     *     </li>
     * </ul>
     */
    private boolean maxSessionsPreventsLogin;

    /**
     * SpringSecurity创建session的策略
     */
    private SessionCreationPolicy sessionPolicy;

    private boolean enableSessionUrlRewriting;

    /**
     * 在执行HttpSession认证策略的时候出现异常执行的处理器
     * 和下面这个一样
     */
    private AuthenticationFailureHandler sessionAuthenticationFailureHandler;

    /**
     * 在执行HttpSession认证策略的时候出现异常跳转了Url
     * 就是创建一个跳转的策略
     */
    private String sessionAuthenticationErrorUrl;
  • 我们先介绍其中的对象

1.1 SessionAuthenticationStrategy

  • SessionAuthenticationStrategy:是在身份认证成功发生时 执行有关HttpSession的策略 默认会有一个防止固定会话攻击的 改变sessionId的策略
public interface SessionAuthenticationStrategy {

   /**
    * 比如说在UsernamePasswordAuthenticationFilter中认证通过了就会执行
    * @param authentication 创建的正确的认证对象,而不是由用户输入的用户名和密码构建的
    */
   void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response)
         throws SessionAuthenticationException;

}
  • 实现类如下,接下来将一个个的讲
    • ChangeSessionIdAuthenticationStrategy
    • SessionFixationProtectionStrategy
    • CsrfAuthenticationStrategy
    • RegisterSessionAuthenticationStrategy
    • ConcurrentSessionControlAuthenticationStrategy

1.1.1 ChangeSessionIdAuthenticationStrategy

  • 在介绍此过滤器之前要先知道会话固定攻击的原理
  • 会话固定攻击:让合法用户使用黑客预先设置的SessionID进行登录,从而让服务器不再进行生成新的SessionID,从而导致黑客设置的SessionId变成了合法桥梁
  • 所以说防止会话固定攻击的原理就是在认证完成后更新SessionID
  • 那现在我们先看下防止会话固定保护的基类AbstractSessionFixationProtectionStrategy的onAuthentication(...)方法
public void onAuthentication(Authentication authentication, HttpServletRequest request,
      HttpServletResponse response) {
   boolean hadSessionAlready = request.getSession(false) != null;
   if (!hadSessionAlready && !this.alwaysCreateSession) {
      // 如果没有会话,就没有会话固定攻击
      return;
   }
   // 如果需要,创建新的会话
   HttpSession session = request.getSession();
   if (hadSessionAlready && request.isRequestedSessionIdValid()) {
      String originalSessionId;
      String newSessionId;
      // 获得此HttpSession的互斥锁
      Object mutex = WebUtils.getSessionMutex(session);
      synchronized (mutex) {
         // We need to migrate to a new session
         originalSessionId = session.getId();
         session = applySessionFixation(request);
         newSessionId = session.getId();
      }
      if (originalSessionId.equals(newSessionId)) {
         this.logger.warn("Your servlet container did not change the session ID when a new session "
               + "was created. You will not be adequately protected against session-fixation attacks");
      }
      else {
         if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Changed session id from %s", originalSessionId));
         }
      }
      // 发布会话ID变更事件
      onSessionChange(originalSessionId, session, authentication);
   }
}
  • 很明显是通过applySessionFixation(request);方法变更了新的Session,而ChangeSessionIdAuthenticationStrategy也重写了此方法
@Override
HttpSession applySessionFixation(HttpServletRequest request) {
   request.changeSessionId();
   return request.getSession();
}
  • 由于会话固定攻击是因为攻击者利用自己的sessionId让正常用户进行登录了,所以让用户登录后更改sessionId就好了

1.1.2 SessionFixationProtectionStrategy

  • 此类和上面的ChangeSessionIdAuthenticationStrategy都是为了防止固定会话攻击的
  • ChangeSessionIdAuthenticationStrategy是直接改变SessionId的,HttpSession对象没有发生变化,还是那一个地址,而SessionFixationProtectionStrategy是直接换一个新的HttpSession
final HttpSession applySessionFixation(HttpServletRequest request) {
   HttpSession session = request.getSession();
   String originalSessionId = session.getId();
   this.logger.debug(LogMessage.of(() -> "Invalidating session with Id '" + originalSessionId + "' "
         + (this.migrateSessionAttributes ? "and" : "without") + " migrating attributes."));
   // 提取此 HttpSession 中的所有数据
   Map<String, Object> attributesToMigrate = extractAttributes(session);
   int maxInactiveIntervalToMigrate = session.getMaxInactiveInterval();
   session.invalidate();
   session = request.getSession(true); // we now have a new session
   this.logger.debug(LogMessage.format("Started new session: %s", session.getId()));
   // 复制数据到新的HttpSession中
   transferAttributes(attributesToMigrate, session);
   if (this.migrateSessionAttributes) {
      session.setMaxInactiveInterval(maxInactiveIntervalToMigrate);
   }
   return session;
}

1.1.3 CsrfAuthenticationStrategy

  • CsrfAuthenticationStrategy:有关Csrf的HttpSession认证策略,是由于CsrfConfigurer注册的
  • 是为了在认证成功后,更换新的csrfToken
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
      HttpServletResponse response) throws SessionAuthenticationException {
   boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
   //如果原来没有csrfToken,那也就不需要换新的csrfToken
   if (containsToken) {
      //清空原csrfToken
      this.csrfTokenRepository.saveToken(null, request, response);
      CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
      //保存新csrfToken
      this.csrfTokenRepository.saveToken(newToken, request, response);
      //将CsrfToken给调用方
      //request中的属性会被SpringMvc的Model操作
      request.setAttribute(CsrfToken.class.getName(), newToken);
      request.setAttribute(newToken.getParameterName(), newToken);
      this.logger.debug("Replaced CSRF Token");
   }
}

1.1.4 RegisterSessionAuthenticationStrategy

  • 此类主要是为当认证完成后将 Principal 和 SessionId 和 SessionInformation绑定起来,也是为了踢出用户做准备
/**
 * 为当前会话注册一个新的SessionInformation
 */
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
      HttpServletResponse response) {
   this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
  • 一个账户可以支持在不同地方认证多次,也就是说一个Principal会对应多个SessionId

1.1.5 ConcurrentSessionControlAuthenticationStrategy

  • ConcurrentSessionControlAuthenticationStrategy:处理并发会话控制的策略
  • 我们可以通过此类配置当某个用户登录的会话超过限制后是否阻止登录
/**
 * 某个用户的会话数达到maximumSessions的时候,是否阻止登录
 * <ul>
 *     <li>
 *           true: 后面登录的用户直接抛出异常
 *     </li>
 *     <li>
 *       false:将最先登录的那个会话对应的SessionInformation直接设置为已过期,那么遇到ConcurrentSessionFilter就会有对应的退出操作了
 *     </li>
 * </ul>
 */
private boolean exceptionIfMaximumExceeded = false;
  • 直接看onAuthentication(...)方法
public void onAuthentication(Authentication authentication, HttpServletRequest request,
      HttpServletResponse response) {
   //先获取最大并发数
   int allowedSessions = getMaximumSessionsForThisUser(authentication);
   //如果是-1表示不限制,那么就直接返回
   if (allowedSessions == -1) {
      // We permit unlimited logins
      return;
   }

   //通过SessionInformation注册中心获得当前用户的所有SessionInformation
   List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
   int sessionCount = sessions.size();
   //如果小于最大并发数据,就可以返回了
   if (sessionCount < allowedSessions) {
      // They haven't got too many login sessions running at present
      return;
   }
   //如果数量相等的情况
   if (sessionCount == allowedSessions) {
      HttpSession session = request.getSession(false);
      if (session != null) {
         //只要当前会话在当前用户的SessionInformation集合里面
         //比如有最大限制2,有A和B,B进到自然能够匹配sessionId,也就可以继续后面的操作了
         for (SessionInformation si : sessions) {
            if (si.getSessionId().equals(session.getId())) {
               return;
            }
         }
      }
      //走到这里表示已经达到最大限制了,而且并不是其中的一个
   }
   //已经超过了最大会话数了,要么踢出某个会话,要么抛出异常
   allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
}
  • 然后我们再看allowableSessionsExceeded(...)方法,当会话数超过了指定限制后有两种阻止登录的操作
    • 抛出异常
    • 踢出最早的会话
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
      SessionRegistry registry) throws SessionAuthenticationException {
   //阻止当前会话登录此用户,直接抛出异常
   if (this.exceptionIfMaximumExceeded || (sessions == null)) {
      throw new SessionAuthenticationException(
            this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
                  new Object[] { allowableSessions }, "Maximum sessions of {0} for this principal exceeded"));
   }
   //踢出最早没有操作的会话
   //对SessionInformation的最后一次操作时间进行排序
   sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
   //需要踢出的数量
   int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
   //拿到需要踢出的SessionInformation
   List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
   //标记这些SessionInformation为已过期
   //这样ConcurrentSessionFilter就会对于这些已过期的SessionInformation对应的会话执行登出操作
   for (SessionInformation session : sessionsToBeExpired) {
      session.expireNow();
   }
}

1.2 SessionRegistry

  • 是SessionInformation的注册中心,维护了所有的关联关系
public interface SessionRegistry {

   List<Object> getAllPrincipals();

   /**
    * 获得当前用户所有的 {@link SessionInformation}
    */
   List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);

   /**
    * 获取指定sessionId的 {@link SessionInformation}。甚至会返回过期的会话(尽管永远不会返回已销毁的会话)
    */
   SessionInformation getSessionInformation(String sessionId);
    
   /**
    * 刷新当前sessionId对应的 {@link SessionInformation} 的最后一次操作时间
    */
   void refreshLastRequest(String sessionId);

   /**
    * 注册新的映射关系, 新注册的会话不会被标记为过期
    */
   void registerNewSession(String sessionId, Object principal);

   /**
    * 清除有关此sessionId的映射关系
    */
   void removeSessionInformation(String sessionId);

}
  • SessionRegistry只有一个实现:SessionRegistryImpl

1.2.1 SessionRegistryImpl

  • 针对SpringSecurity内部维护的SessionInformation的操作 比如说用户登录后,才会创建会话session,那么通常会有一个SpringSecurity的SessionInformation的创建
  • 此类的核心在于如何维护关联关系,重点就在于下面这两个Map
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {
    /**
     * user和HttpSessionId的映射关系
     * 注意:user类是重写了equals方法的,这样就能存储某个用户对应的所有sessionId了
     */
    private final ConcurrentMap<Object, Set<String>> principals;

    /**
     * 是sessionId到SessionInformation的映射关系
     */
    private final Map<String, SessionInformation> sessionIds;
}
  • 通过上面这两个Map我们可以通过用户对象(principal)得到此用户已经登录了哪些会话,然后就可以通过会话Id获取对应的SessionInformation了
  • registerNewSession(...):注册新的映射关系
public void registerNewSession(String sessionId, Object principal) {
   Assert.hasText(sessionId, "SessionId required as per interface contract");
   Assert.notNull(principal, "Principal required as per interface contract");
   //如果原来还有就先删除映射关系
   if (getSessionInformation(sessionId) != null) {
      removeSessionInformation(sessionId);
   }
   if (this.logger.isDebugEnabled()) {
      this.logger.debug(LogMessage.format("Registering session %s, for principal %s", sessionId, principal));
   }
   //保存新的映射关系
   this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
   //这个方法的意思是当principal不在principals中,
   this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
      if (sessionsUsedByPrincipal == null) {
         sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
      }
      sessionsUsedByPrincipal.add(sessionId);
      this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
      return sessionsUsedByPrincipal;
   });
}
  • removeSessionInformation(...):清除有关sessionId的映射关系
public void removeSessionInformation(String sessionId) {
   Assert.hasText(sessionId, "SessionId required as per interface contract");
   //拿到SessionInformation
   SessionInformation info = getSessionInformation(sessionId);
   //为空就不需要清除了
   if (info == null) {
      return;
   }
   if (this.logger.isTraceEnabled()) {
      this.logger.debug("Removing session " + sessionId + " from set of registered sessions");
   }
   //清除映射关系
   this.sessionIds.remove(sessionId);
   //由于principals相当于用户名到SessionId集合的映射
   //所以只需要删除旧的一个就可以了
   this.principals.computeIfPresent(info.getPrincipal(), (key, sessionsUsedByPrincipal) -> {
      this.logger.debug(
            LogMessage.format("Removing session %s from principal's set of registered sessions", sessionId));
      //删除这个SessionId集合中的某一个SessionId
      sessionsUsedByPrincipal.remove(sessionId);
      if (sessionsUsedByPrincipal.isEmpty()) {
         // No need to keep object in principals Map anymore
         this.logger.debug(LogMessage.format("Removing principal %s from registry", info.getPrincipal()));
         sessionsUsedByPrincipal = null;
      }
      this.logger.trace(
            LogMessage.format("Sessions used by '%s' : %s", info.getPrincipal(), sessionsUsedByPrincipal));
      return sessionsUsedByPrincipal;
   });
}
  • refreshLastRequest(..):刷新当前sessionId对应的SessionInformation的最后一次操作时间
public void refreshLastRequest(String sessionId) {
   Assert.hasText(sessionId, "SessionId required as per interface contract");
   //获得对应的SessionInformation
   SessionInformation info = getSessionInformation(sessionId);
   if (info != null) {
      info.refreshLastRequest();
   }
}
  • getAllSessions(...):获得当前用户所有的SessionInformation
public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
   //先获取当前用户名下的所有SessionId
   Set<String> sessionsUsedByPrincipal = this.principals.get(principal);
   if (sessionsUsedByPrincipal == null) {
      return Collections.emptyList();
   }
   //获得当前用户名下的所有SessionInformation
   List<SessionInformation> list = new ArrayList<>(sessionsUsedByPrincipal.size());
   for (String sessionId : sessionsUsedByPrincipal) {
      SessionInformation sessionInformation = getSessionInformation(sessionId);
      if (sessionInformation == null) {
         continue;
      }
      //过期了是否需要添加进去
      if (includeExpiredSessions || !sessionInformation.isExpired()) {
         list.add(sessionInformation);
      }
   }
   return list;
}
  • onApplicationEvent(...):处理session无效而导致的销毁事件和sessionId发生改变的事件
public void onApplicationEvent(AbstractSessionEvent event) {
   //处理销毁事件
   if (event instanceof SessionDestroyedEvent) {
      SessionDestroyedEvent sessionDestroyedEvent = (SessionDestroyedEvent) event;
      String sessionId = sessionDestroyedEvent.getId();
      //清除有关sessionId的映射关系
      removeSessionInformation(sessionId);
   }
   //处理sessionId发生改变事件
   //比如说为了防止会话固定攻击的ChangeSessionIdAuthenticationStrategy,就会在认证成功后改变SessionId
   else if (event instanceof SessionIdChangedEvent) {
      SessionIdChangedEvent sessionIdChangedEvent = (SessionIdChangedEvent) event;
      String oldSessionId = sessionIdChangedEvent.getOldSessionId();
      //要确保旧sessionId在session注册中心中
      if (this.sessionIds.containsKey(oldSessionId)) {
         Object principal = this.sessionIds.get(oldSessionId).getPrincipal();
         //清除有关旧sessionId的映射关系
         removeSessionInformation(oldSessionId);
         //注册新的映射关系
         registerNewSession(sessionIdChangedEvent.getNewSessionId(), principal);
      }
   }
}
  • 默认我们只开启并发管理的情况下,是无法接受到AbstractSessionEvent事件的,需要往容器注册一个HttpSessionEventPublisher才可以
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
   return new HttpSessionEventPublisher();
}

1.3 SessionCreationPolicy

  • SpringSecurity的过滤器在执行过程中是否允许创建会话的策略,在SecurityContextPersistenceFilter中会被用到
public enum SessionCreationPolicy {

   /**
    * 总是 {@link HttpSession}
    */
   ALWAYS,

   /**
    * 永远不会创建 {@link HttpSession}, 除非他已经存在
    * 应该不会由Spring Security创建
    */
   NEVER,

   /**
    * 在需要的时候创建 {@link HttpSession}
    */
   IF_REQUIRED,

   /**
    * Spring Security永远不会创建 {@link HttpSession},也永远不会使用它获取 {@link HttpSession}
    */
   STATELESS

}

1.4 init(...)

public void init(H http) {
   //获得安全上下文存储库
   SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
   //判断是否完全不需要创建session
   boolean stateless = isStateless();
   if (securityContextRepository == null) {
      if (stateless) {
         //如果没有安全上下文存储库,又不需要创建session
         //那么就将安全上下文存储库设置为NullSecurityContextRepository
         //那么程序在执行的时候就无法判断当前用户的认证信息了
         http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
      }
      else {
         //默认安全上下文存储库的策略是基于HttpSession的
         HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
         httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
         httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
         //设置认证对象分析器
         AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
         if (trustResolver != null) {
            httpSecurityRepository.setTrustResolver(trustResolver);
         }
         //将HttpSession级别的安全上下文策略保存到SharedObject中
         http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
      }
   }
   //从SharedObject中获取请求缓冲器
   RequestCache requestCache = http.getSharedObject(RequestCache.class);
   if (requestCache == null) {
      //如果没有请求缓冲器并且又不需要创建HttpSession,那就注入一个空实现的请求缓冲器
      //因为请求缓冲器的有效实现类只有HttpSessionRequestCache和CookieRequestCache
      //CookieRequestCache不需要存储在服务端,而HttpSessionRequestCache是基于HttpSession,没有HttpSession也就不用请求缓冲器了
      if (stateless) {
         http.setSharedObject(RequestCache.class, new NullRequestCache());
      }
   }
   //设置HttpSession认证策略
   http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy(http));
   //设置HttpSession过期(无效)策略
   http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
}
  • getSessionAuthenticationStrategy(...):获得HttpSession认证策略
private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
   //如果以前执行过本方法那么这个就不为空
   if (this.sessionAuthenticationStrategy != null) {
      return this.sessionAuthenticationStrategy;
   }
   //获得用户设置过的session认证策略
   List<SessionAuthenticationStrategy> delegateStrategies = this.sessionAuthenticationStrategies;
   SessionAuthenticationStrategy defaultSessionAuthenticationStrategy;
   //默认session策略取哪一个
   //可以看出一个会执行postProcess方法
   if (this.providedSessionAuthenticationStrategy == null) {
      defaultSessionAuthenticationStrategy = postProcess(this.sessionFixationAuthenticationStrategy);
   }
   else {
      defaultSessionAuthenticationStrategy = this.providedSessionAuthenticationStrategy;
   }

   //是否需要限制用户的并发数
   if (isConcurrentSessionControlEnabled()) {
      //获得SessionInformation注册中心
      SessionRegistry sessionRegistry = getSessionRegistry(http);
      //创建一个处理并发会话控制的策略
      ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlStrategy = new ConcurrentSessionControlAuthenticationStrategy(
            sessionRegistry);
      concurrentSessionControlStrategy.setMaximumSessions(this.maximumSessions);
      concurrentSessionControlStrategy.setExceptionIfMaximumExceeded(this.maxSessionsPreventsLogin);
      concurrentSessionControlStrategy = postProcess(concurrentSessionControlStrategy);

      //创建一个注册SessionInformation的策略
      RegisterSessionAuthenticationStrategy registerSessionStrategy = new RegisterSessionAuthenticationStrategy(
            sessionRegistry);
      registerSessionStrategy = postProcess(registerSessionStrategy);

      //通常来说是添加一个防止固定会话攻击的策略
      delegateStrategies.addAll(Arrays.asList(concurrentSessionControlStrategy,
            defaultSessionAuthenticationStrategy, registerSessionStrategy));
      //也就说只要开启了限制会话并发数,那么就至少有这三个策略
   }
   else {
      //否则默认就只有防止固定会话攻击的策略
      delegateStrategies.add(defaultSessionAuthenticationStrategy);
   }
   //变成一个混合型的HttpSession认证策略,并设置到对应的位置上
   this.sessionAuthenticationStrategy = postProcess(
         new CompositeSessionAuthenticationStrategy(delegateStrategies));
   return this.sessionAuthenticationStrategy;
}

1.5 configure(...)

public void configure(H http) {
   //获得HttpSession级别的安全上下文存储策略
   SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
   //创建第一个过滤器
   SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(securityContextRepository,
         getSessionAuthenticationStrategy(http));

   //设置出现异常跳转的Url
   if (this.sessionAuthenticationErrorUrl != null) {
      sessionManagementFilter.setAuthenticationFailureHandler(
            new SimpleUrlAuthenticationFailureHandler(this.sessionAuthenticationErrorUrl));
   }

   //获得HttpSession过期(无效)策略
   InvalidSessionStrategy strategy = getInvalidSessionStrategy();
   if (strategy != null) {
      sessionManagementFilter.setInvalidSessionStrategy(strategy);
   }

   //获得执行HttpSession认证策略的时候出现异常的 失败策略
   AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
   if (failureHandler != null) {
      sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
   }

   //获得认证对象解析器
   AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
   if (trustResolver != null) {
      sessionManagementFilter.setTrustResolver(trustResolver);
   }

   //大部分Filter必执行的postProcess方法
   sessionManagementFilter = postProcess(sessionManagementFilter);
   //添加到HttpSecurity的过滤器集合中
   http.addFilter(sessionManagementFilter);
   //当开启了并发会话的限制
   if (isConcurrentSessionControlEnabled()) {
      //创建第二个过滤器
      ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);

      concurrentSessionFilter = postProcess(concurrentSessionFilter);
      http.addFilter(concurrentSessionFilter);
   }
}
  • createConcurrencyFilter(...):创建一个有关并发会话限制的过滤器
private ConcurrentSessionFilter createConcurrencyFilter(H http) {
   //获得SessionInformation过期策略
   SessionInformationExpiredStrategy expireStrategy = getExpiredSessionStrategy();
   //获得SessionInformation注册中心
   SessionRegistry sessionRegistry = getSessionRegistry(http);

   //创建过滤器
   ConcurrentSessionFilter concurrentSessionFilter = (expireStrategy != null)
         ? new ConcurrentSessionFilter(sessionRegistry, expireStrategy)
         //虽然这里没传SessionInformation过期策略,但是构造方法实际上创建了默认的
         : new ConcurrentSessionFilter(sessionRegistry);

   //重点:从httpSecurity中获得登出配置类
   LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
   if (logoutConfigurer != null) {
      //拿到登出处理器
      List<LogoutHandler> logoutHandlers = logoutConfigurer.getLogoutHandlers();
      if (!CollectionUtils.isEmpty(logoutHandlers)) {
         //设置到ConcurrentSessionFilter过滤器中了,这样这个过滤器就可以做登出操作了
         concurrentSessionFilter.setLogoutHandlers(logoutHandlers);
      }
   }
   return concurrentSessionFilter;
}

2. ConcurrentSessionFilter

  • 此过滤器是限制并发会话的过滤器,作用如下
    • 作用1:对于已经过期的SessionInformation进行登出操作,然后执行过期策略
    • 作用2:更新操作时间,这是因为如果并发数达到限制,可以会根据最后操作时间来踢出用户
  • 直接看核心的doFilter(...)方法
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   HttpSession session = request.getSession(false);
   if (session != null) {
      //获得当前会话对应的SessionInformation
      SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
      if (info != null) {
         //如果过期了
         if (info.isExpired()) {
            // Expired - abort processing
            this.logger.debug(LogMessage
                  .of(() -> "Requested session ID " + request.getRequestedSessionId() + " has expired."));
            //执行登出操作
            doLogout(request, response);
            //执行SessionInformation过期策略
            this.sessionInformationExpiredStrategy
                  .onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response));
            return;
         }
         //如果没有过期,那么就更新操作时间
         //这是因为如果并发数达到限制,可以根据最后操作时间来踢出用户
         this.sessionRegistry.refreshLastRequest(info.getSessionId());
      }
   }
   chain.doFilter(request, response);
}
  • 这里重点就在于如何知道这个SessionInformation过期了呢?
  • 我们可以回到ConcurrentSessionControlAuthenticationStrategy的allowableSessionsExceeded(...)方法中 image.png
  • 这里是发生在用户认证完成后,超过了最大会话数的时候会将某些SessionInformation直接设置为已过期,所以说知道过滤器的顺序是十分重要,具体顺序见FilterComparator类

3. SessionManagementFilter

  • 检测从执行 SecurityContextPersistenceFilter 到执行本过滤器之间是否就已经通过身份验证 如果已经通过,那么就执行HttpSession认证策略和检查HttpSession是否已经无效
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   if (request.getAttribute(FILTER_APPLIED) != null) {
      chain.doFilter(request, response);
      return;
   }
   //标记为当前request已经执行过
   request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
   //没有在HttpSession级别安全上下文策略找到安全上下文
   if (!this.securityContextRepository.containsContext(request)) {
      Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      //但是在线程级别安全上下文策略中找到,并且是一个不是一个匿名用户
      //比如通过rememberMe登录的,因为
      if (authentication != null && !this.trustResolver.isAnonymous(authentication)) {
         //用户在当前请求中已经通过身份验证,因此调用HttpSession认证策略
         //比如说rememberMe的方法进行登录,因为RememberMeAuthenticationFilter在SecurityContextPersistenceFilter之前,在本过滤器之后执行
         try {
            //执行HttpSession认证策略
            this.sessionAuthenticationStrategy.onAuthentication(authentication, request, response);
         }
         catch (SessionAuthenticationException ex) {
            //会话策略可以拒绝认证,比如说并发会话可能会抛出异常
            this.logger.debug("SessionAuthenticationStrategy rejected the authentication object", ex);
            //清空线程级别的安全上下文
            SecurityContextHolder.clearContext();
            //执行失败策略
            this.failureHandler.onAuthenticationFailure(request, response, ex);
            return;
         }
         //紧急在HttpSession级别的安全上下文存储策略中保存一个空的安全上下文
         //我猜是通过记住我认证的时候没有将SecurityContext保存在SecurityContextRepository中,所以这里紧急保存下
         this.securityContextRepository.saveContext(SecurityContextHolder.getContext(), request, response);
      }
      else {
         //没有安全上下文或者是一个匿名用户的时候,检查会话过期(无效)
         if (request.getRequestedSessionId() != null && !request.isRequestedSessionIdValid()) {
            if (this.logger.isDebugEnabled()) {
               this.logger.debug(LogMessage.format("Request requested invalid session id %s",
                     request.getRequestedSessionId()));
            }
            if (this.invalidSessionStrategy != null) {
               //执行HttpSession过期(无效)策略
               this.invalidSessionStrategy.onInvalidSessionDetected(request, response);
               return;
            }
         }
      }
   }
   chain.doFilter(request, response);
}
  • 这段代码的核心在于要执行HttpSession认证策略,也就说第19行代码
  • 我们想象一下用表单认证的情况也就说使用UsernamePasswordAuthenticationFilter进行认证
    • 当认证完成后我们就会执行下面的方法然后就会将认证对象保存在线程中(默认的实现) image.png
    • 然后再借助RequestCache机制,将当前请求重定向到原请求上 image.png
    • 然后我们可以发现在UsernamePasswordAuthenticationFilter中不会执行下一个过滤器 image.png
    • 然后在SecurityContextPersistenceFilter.doFilter(...)中的finally代码块中就会将SecurityContext保存在Session中(默认实现) image.png
    • 所以说在表单认证方式下可能存在 认证对象在HttpSession中不存在,而在线程中存在吗?,肯定是不可能是
  • 但是其他认证方式呢?比如说是通过记住我令牌认证的就会有这种情况
  • 因为在RememberMeAuthenticationFilter中不会执行HttpSession认证策略,所以说需要在SessionManagementFilter中去执行