#yyds干货盘点#会话管理

Posted 周杰伦本人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点#会话管理相关的知识,希望对你有一定的参考价值。

会话管理

当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话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 hasnt 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 havent 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,在该过滤器中,判断当前会话是否过期,如果过期执行注销流程,如果没有过期,更新最近一次请求时间。

以上是关于#yyds干货盘点#会话管理的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点#Git实现分支管理

Session与Token认证机制 前后端分离下如何登录 #yyds干货盘点#

#yyds干货盘点#内核编译和管理

#yyds干货盘点#安装悟空CRM11

#yyds干货盘点# Java | 关于synchronized相关理解

#yyds干货盘点# mybatis源码解读:executor包(语句处理功能)