Spring Security的会话管理

网友投稿 775 2022-05-30

Spring Security的会话管理

当浏览器调用登录接口登录成功后,服务端会和浏览器之间建立一个会话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 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 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 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); }

从sessionRegistry中获取当前用户所有未失效的SessionInformation,然后获取当前项目允许的最大session数。如果获取到的SessionInformation实例小于当前项目允许的最大session数,说明当前登录没有问题,直接return

如果允许的最大session数为-1,表示应用并不限制登录并发数,当前登录没有问题,直接return

如果两者相等,判断当前sessionId是否在SessionInformation中,如果存在,直接return

超出最大并发数,进入allowableSessionsExceeded方法

protected void allowableSessionsExceeded(List 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 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登录,进而获取用户登录身份。

Spring Security的会话管理

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

SessionManagementFilter创建过程中调用getSessionAuthenticationStrategy方法获取SessionAuthenticationStrategy的实例放入过滤器中,然后配置各种回调函数,最终创建的SessionManagementFilter过滤器放入HttpSecurity中。

如果开启会话并发控制(只要maximumSessions不会空就算开启会话并发控制),则创建ConcurrentSessionFilter过滤器 加入到HttpSecurity中。

总结

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

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

统一身份认证服务 IAM

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:第2章TCPIP协议 网络层arp 网络接口层
下一篇:AQS源码学习
相关文章