#yyds干货盘点#会话管理

人生必须的知识就是引人向光明方面的明灯。这篇文章主要讲述#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 numberallowableSessionsExceeded(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,它代理了三个类ConcurrentSessionControlAuthenticationStrategyChangeSessionIdAuthenticationStrategy RegisterSessionAuthenticationStrategy。当前请求在这三个SessionAuthenticationStrategy中分别走一圈,第一个用来判断当前用户的Session数是否超过限制,第二个用来修改sessionId(防止会话固定攻击),第三个用来将当前Session注册到SessionRegistry中。
【#yyds干货盘点#会话管理】如果用户使用RememberMe的方式进行身份认证,则会通过SessionManagementFilter的doFilter方法触发Session并发管理。当用户认证成功后,以后的每一次请求都会经过ConcurrentSessionFilter,在该过滤器中,判断当前会话是否过期,如果过期执行注销流程,如果没有过期,更新最近一次请求时间。

    推荐阅读