写点什么

SpringSecurity 会话管理

作者:周杰伦本人
  • 2022 年 7 月 04 日
  • 本文字数:7973 字

    阅读完需:约 26 分钟

SpringSecurity 会话管理

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

❤️ 感谢大家

如果你觉得这篇内容对你挺有有帮助的话:


  1. 欢迎关注我❤️,点赞👍🏻,评论🤤,转发🙏

  2. 关注盼盼小课堂,定期为你推送好文,还有群聊不定期抽奖活动,可以畅所欲言,与大神们一起交流,一起学习。

发布于: 刚刚阅读数: 4
用户头像

还未添加个人签名 2020.02.29 加入

公众号《盼盼小课堂》,多平台优质博主

评论

发布
暂无评论
SpringSecurity会话管理_7月月更_周杰伦本人_InfoQ写作社区