Spring Security之Session管理

前言

在聊认证过滤器的时候,我们埋了个坑:Session管理。实际上,事情从这里开始,就变得复杂了。提前跟大家交个底:后续我们将涉及多个需要协同才能完成的功能。

什么是Session

想要管理session,就必须搞清楚Session的来龙去脉。

Session的产生

我们都知道HTTP协议本身是一种无状态的协议,即每个请求都是独立的,服务器不会记住前一个请求的信息。然而,在Web应用中,经常需要跟踪用户的会话状态,以便提供个性化的服务和保持用户数据的连续性。

为了弥补HTTP协议的无状态性,Session机制应运而生。Session是一种在客户端和服务器之间保持状态的机制,通过在服务器端存储用户的状态信息,并在每个请求中传递一个唯一的会话标识符(通常是JSESSIONID),使得服务器能够识别来自同一个客户端的多个请求,并为其提供个性化的响应。

在Web应用中,当客户端首次访问服务器时,服务器会为其创建一个新的Session对象,并生成一个与之关联的会话标识符。这个标识符会通过特定的方式(如cookies或URL重写)传递给客户端,并在后续的请求中由客户端发送回服务器。服务器根据会话标识符从Session池中检索出对应的Session对象,从而获取用户的会话状态信息。

总结一下:

  • Session是服务器用来跟踪用户状态的一种机制
  • 当用户访问Web应用时,服务器会为用户创建一个唯一的Session ID,并通过Cookie或其他方式将其发送给用户。
  • 在后续的请求中,用户会携带这个Session ID,服务器通过解析这个ID来识别用户,并恢复用户的会话状态。

Session的安全威胁

  1. 会话劫持(Session Hijacking)

    攻击者通过各种手段(如网络嗅探、跨站脚本攻击等)获取到用户的Session ID,然后利用这个有效的Session ID伪装成合法用户,访问用户的敏感信息或执行恶意操作。

  2. 会话固定(Session Fixation)

    攻击者预先在用户的目标服务器上创建一个有效的Session,然后通过各种手段诱使用户使用这个Session。这样,攻击者就可以在用户正常登录后接管用户的会话,获取用户的敏感信息。

  3. 跨站请求伪造(CSRF)

    攻击者诱导用户访问一个恶意网站,该网站包含指向目标网站的恶意请求,由于用户的浏览器会自动带上用户的Session信息,因此这个恶意请求会被视为用户的合法请求,从而导致用户的数据被篡改或泄露。

  4. 中间人攻击

    攻击者通过拦截客户端和服务器之间的通信,修改或窃取Session信息,从而获取用户的敏感数据或执行恶意操作。

Session管理

有了概念上的铺垫,结合《Spring Security之认证过滤器》,我们初步理解一下Session管理的作用。

而在Spring Security中,哪些功能才算呢?这得从SessionManagermentFilter的核心组件来看:SessionAuthenticationStrategy。

SessionAuthenticationStrategy

该组件的定位是:在认证(成功)时,为HttpSession相关行为提供插件式支持。

子类 作用 描述
AbstractSessionFixationProtectionStrategy 固定会话防御策略 用于防御固定会话攻击
CsrfAuthenticationStrategy 跨域请求伪造防御策略 为了确保认证前的csrfToken与认证后不一样,需要在认证成功后重置crsfToken。
RegisterSessionAuthenticationStrategy 注册当前认证成功的Session 可以用于追踪session,也可以统计当前在线session数量,乃至于协助控制某个用户同时在线数量
ConcurrentSessionControlAuthenticationStrategy session并发控制 用于控制某个用户同时在线数量,需要搭配前者使用
  • AbstractSessionFixationProtectionStrategy

    他的核心逻辑主要是:

    如果不存在session,也就没有防御的必要。

    加同步锁后执行子类防御逻辑

    发布session事件

    具体策略有两个:

    ChangeSessionIdAuthenticationStrategy 只修改sessionId,这也是默认策略
    SessionFixationProtectionStrategy 把原session失效,再重建session,并拷贝原session中的属性

    前者的核心主要是调用:HttpSession#changeSessionId()方法。

    后者的核心则包括:让当前session失效-HttpSession#invalidate(); 重建session-request.getSession(true),然后拷贝将原session中的属性拷贝到新session中。

    实际上对于高版本的Tomcat(包括SpringBoot内置的,支持Servlet3.1及以上版本即可),二者在实际效果上并没有区别。因为前者改变了sessionId,内容不变。而后者重建了session,自然sessionId也是发生变化了的,又将原来session的相关属性复制过来。因此也相当于内容不变,只改了sessionId。

  • CsrfAuthenticationStrategy

    很明显,这与跨域请求有关。不难猜测,跨域请求防御必定是个需要多个Filter协作的过滤器。但这里我们只聊他在这里的作用,聊到跨域请求防御再把这块串起来。

    java 复制代码
    public class CsrfAuthenticationStrategy implements SessionAuthenticationStrategy {
    	@Override
    	public void onAuthentication(Authentication authentication, HttpServletRequest request,
    			HttpServletResponse response) throws SessionAuthenticationException {
    		boolean containsToken = this.csrfTokenRepository.loadToken(request) != null;
    		// 存在csrfToken则重置
    		if (containsToken) {
    			// 先清空:存入null
    			this.csrfTokenRepository.saveToken(null, request, response);
    			// 再重新创建一个csrfToken
    			CsrfToken newToken = this.csrfTokenRepository.generateToken(request);
    			// 保存到csrfToken仓库
    			this.csrfTokenRepository.saveToken(newToken, request, response);
    			// 放入request中,以便可能的页面渲染。
    			request.setAttribute(CsrfToken.class.getName(), newToken);
    			request.setAttribute(newToken.getParameterName(), newToken);
    		}
    	}
    }

    PS: CsrfTokenRepository是跨域防御的核心组件,跳过。

  • RegisterSessionAuthenticationStrategy

    这个主要靠SessionRegistry来实现。而SessionRegistry也是后面session并发控制的关键。因此他也是个协同组件哦。只不过不是在Filter层面,而是跟ConcurrentSessionControlAuthenticationStrategy协同。前者,用于登记注册,后者则利用登记的session进行统计,并以此决定后续操作。

    java 复制代码
    public interface SessionRegistry {
    	// 获取所有(在线的/离线但尚未清理的-session层面的)用户
        List<Object> getAllPrincipals();
    	// 获取所有session(支持查询已超时的session)
        List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions);
    	// 根据sessionId获取Session信息
        SessionInformation getSessionInformation(String sessionId);
    	// 根据sessionId刷新上一次请求,这与自定义的SessionInformation有关。
        void refreshLastRequest(String sessionId);
    	// 注册新的session
        void registerNewSession(String sessionId, Object principal);
    	// 根据sessionId移除SessionInformation 
        void removeSessionInformation(String sessionId);
    }

    RegisterSessionAuthenticationStrategy自然使用的是registerNewSession方法进行登记。

  • ConcurrentSessionControlAuthenticationStrategy

    java 复制代码
    public class ConcurrentSessionControlAuthenticationStrategy
    		implements MessageSourceAware, SessionAuthenticationStrategy {
    	@Override
    	public void onAuthentication(Authentication authentication, HttpServletRequest request,
    			HttpServletResponse response) {
    		// 获取当前用户的最大session限制。这里是配置好的,不是动态变化的,除非定制。
    		int allowedSessions = getMaximumSessionsForThisUser(authentication);
    		if (allowedSessions == -1) {
    			// -1 则表示不限制,因此直接放行。
    			return;
    		}
    		// 需要限制用户的session个数
    		List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
    		int sessionCount = sessions.size();
    		if (sessionCount < allowedSessions) {
    			// 尚未达到多点登录的限制,允许登录
    			return;
    		}
    		if (sessionCount == allowedSessions) {
    			HttpSession session = request.getSession(false);
    			if (session != null) {
    				// 达到限制,但属于是当前已在线的session,是正常请求。
    				for (SessionInformation si : sessions) {
    					if (si.getSessionId().equals(session.getId())) {
    						return;
    					}
    				}
    			}
    		}
    		// 超出限制,且又来了新的session
    		allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
    	}
    	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"));
    	}
    	// 通过lru的方式,得到最近使用时间最远的session,并使其失效
    	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();
    	}
    }

问题延伸

现在我们知道SessionAuthenticationStrategy的作用,也知道他可以实现哪些所谓的"与Session相关的操作"了。在这里,我要问大家一个问题:

如果我们不用SessionAuthenticationStrategy能正常登录吗?

很显然,我们依然能够正常登录,只不过与之相关的固定会话防御、跨域防御、并发会话控制功能无法实现。

SessionManagementFilter

我们先看看类注释:

检测在请求开始开始时用户已经是否已经认证过了,如果已认证,则调用配置好的SessionAuthenticationStrategy来完成session相关的活动,例如:固定会话保护机制和多请求并发登录检查。

与session相关的活动,我认为可以分为两类:

  • 与session相关的安全防御活动。例如:固定会话保护机制、跨域请求伪造防御
  • 与session相关的功能。例如:session并发控制(多点登录)

以上行为/活动,无疑被封装到SessionAuthenticationStrategy的实现之中。

实现原理

可以看出,条件很多,核心功能有两点:一是session超时策略。二是SessionAuthenticationStrategy、SecurityContextRepository两个组件的执行。

小结

SessionManagementFilter会关注session超时,当session超时时会自动跳转到指定的页面。而其本身设计的重点是,执行SessionAuthenticationStrategy的相关策略 。至于SecurityContextRepository,这个与BUG
SEC-1396有关。

与认证过滤器的协同

准确来说,应该说是与部分认证过滤器的协同。因为我们的UsernamePasswordAuthenticationFilter,或者说任何实现了AbstractAuthenticationProcessingFilter,其本身就会执行SessionAuthenticationStrategy

对于UsernamePasswordAuthenticationFilter,个人认为只有在并发请求、session超时管理情况下才能派上用场。如果不使用SessionManagementFilter的话,我们要配置SessionAuthenticationStrategy可以自己new相应的策略在放到HttpSecurity的共享对象中就行。但session超时的请求如果想要处理,例如跳转登录页面,就得我们自己处理了。

而像AbstractPreAuthenticatedProcessingFilterRememberMeAuthenticationFilterBasicAuthenticationFilter等,就不会执行这个。甚至,也不会执行SecurityContextRepository,因此就必须要SessionManagementFilter的协助。至于为什么UsernamePasswordAuthenticationFilter这么特别,我只能猜测他可能是先实现的,而后者是后面设计的。

SessionManagementConfigurer

这是负责配置session管理配置器。发布的过滤器除了SessionManagementFilter,还有ConcurrentSessionFilter。这也是SessionManagement容易令人迷糊的地方,明明名字都一样,却不是一一对应。

java 复制代码
public final class SessionManagementConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<SessionManagementConfigurer<H>, H> {

	@Override
	public void init(H http) {
		SecurityContextRepository securityContextRepository = http.getSharedObject(SecurityContextRepository.class);
		boolean stateless = isStateless();
		if (securityContextRepository == null) {
			// 设置SecurityContextRepository
			if (stateless) {
				// 无状态应用设置为NullSecurityContextRepository
				http.setSharedObject(SecurityContextRepository.class, new NullSecurityContextRepository());
			}
			else {
				// 默认为HttpSessionSecurityContextRepository
				HttpSessionSecurityContextRepository httpSecurityRepository = new HttpSessionSecurityContextRepository();
				httpSecurityRepository.setDisableUrlRewriting(!this.enableSessionUrlRewriting);
				httpSecurityRepository.setAllowSessionCreation(isAllowSessionCreation());
				AuthenticationTrustResolver trustResolver = http.getSharedObject(AuthenticationTrustResolver.class);
				if (trustResolver != null) {
					// 确保httpSecurityRepository使用的是共享对象trustResolver
					httpSecurityRepository.setTrustResolver(trustResolver);
				}
				http.setSharedObject(SecurityContextRepository.class, httpSecurityRepository);
			}
		}
		RequestCache requestCache = http.getSharedObject(RequestCache.class);
		if (requestCache == null) {
			if (stateless) {
				// 无状态应用设置NullRequestCache
				http.setSharedObject(RequestCache.class, new NullRequestCache());
			}
		}
		// SessionAuthenticationStrategy设置为共享对象
		http.setSharedObject(SessionAuthenticationStrategy.class, getSessionAuthenticationStrategy(http));
		// InvalidSessionStrategy设置为共享对象
		http.setSharedObject(InvalidSessionStrategy.class, getInvalidSessionStrategy());
	}

	@Override
	public void configure(H http) {
		// 创建SessionManagementFilter 
		SessionManagementFilter sessionManagementFilter = createSessionManagementFilter(http);
		if (sessionManagementFilter != null) {
			http.addFilter(sessionManagementFilter);
		}
		// 开启session并发控制,需要创建ConcurrentSessionFilter 
		if (isConcurrentSessionControlEnabled()) {
			ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http);

			concurrentSessionFilter = postProcess(concurrentSessionFilter);
			http.addFilter(concurrentSessionFilter);
		}
		if (!this.enableSessionUrlRewriting) {
			// 禁止url编码,这个涉及到重定向-其实就是封装一下请求
			http.addFilter(new DisableEncodeUrlFilter());
		}
		if (this.sessionPolicy == SessionCreationPolicy.ALWAYS) {
			// 确保session存在。就是在请求进入的第一时间,获取一下session。
			http.addFilter(new ForceEagerSessionCreationFilter());
		}
	}
	
	private SessionManagementFilter createSessionManagementFilter(H http) {
		if (shouldRequireExplicitAuthenticationStrategy()) {
			return null;
		}
		// 对于stateful应用直接使用HttpSessionSecurityContextRepository
		SecurityContextRepository securityContextRepository = this.sessionManagementSecurityContextRepository;
		// 获取配置的SessionAuthenticationStrategy,创建SessionManagementFilter
		// 总的来说,SessionManagementConfigurer的重中之重就是配置SessionAuthenticationStrategy
		// 这也是SessionManagementFilter的核心逻辑所在
		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.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
		return postProcess(sessionManagementFilter);
	}
	private SessionAuthenticationStrategy getSessionAuthenticationStrategy(H http) {
		if (this.sessionAuthenticationStrategy != null) {
			// 避免重复创建
			return this.sessionAuthenticationStrategy;
		}
		List<SessionAuthenticationStrategy> delegateStrategies = this.sessionAuthenticationStrategies;
		SessionAuthenticationStrategy defaultSessionAuthenticationStrategy;
		if (this.providedSessionAuthenticationStrategy == null) {
			// 用户没有配置SessionAuthenticationStrategy默认为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);
		}
		// 将策略通过CompositeSessionAuthenticationStrategy组装
		this.sessionAuthenticationStrategy = postProcess(
				new CompositeSessionAuthenticationStrategy(delegateStrategies));
		return this.sessionAuthenticationStrategy;
	}

}

这里主要两个方法:

init方法

该方法可能会创建共享对象:

共享对象 释义 stateless场景 非stateless场景
SecurityContextRepository 安全上下文仓库,一般将安全上下文保存在session中 设置RequestAttributeSecurityContextRepository为共享对象,而sessionManagementFilter使用NullSecurityContextRepository HttpSessionSecurityContextRepository,共享对象与sessionManagementFilter使用的是同一个对象
RequestCache 请求缓存,与认证成功后,跳转上一次请求有关。 创建NullRequestCache
SessionAuthenticationStrategy session认证策略 与session相关的活动
InvalidSessionStrategy session超时策略 session超时后的处理

从这里也能看出,session管理必须要识别应用是否为stateless应用。因为对于stateless应用,是没有会话概念的。自然session管理也就没有这个必要了。stateless是通过token来维持类似于会话的功能的,这个token可能是存在于header也可能存在于RequestAttribute(默认选择)中。这也就解释了SecurityContextRepository的创建。

  • configure方法
    这个方法最主要的是创建SessionManagementFilter,而创建SessionManagementFilter的核心是创建SessionAuthenticationStrategy。
    第一层是session的创建策略,前面仔细聊过,不多逻辑。如果没有指定,则会使用修改sessionId的策略。第二层是session并发控制。这个会涉及SessionRegistry,会优先从httpSecurity的共享对象中获取,如果没有就创建一个,同时注册为监听器。没错,他就是利用监听机制完成session的注册和注销的。然后将SessionRegistry封装成RegisterSessionAuthenticationStrategy;构建好所有的SessionAuthenticationStrategy之后,就通过组合模式,封装成CompositeSessionAuthenticationStrategy统一调用。
    除了以上的核心逻辑外,SessionManagementFilter还有session超时处理所涉及的:超时处理器或者超时处理策略;SessionAuthenticationStrategy处理失败的处理器。以及与判断是否需要执行sessionManage逻辑的组件:SecurityContextHolderStrategy、TrustResolver、SecurityContextRepository。
    对于SessionManagementConfigurer来说,还有ConcurrentSessionFilter、DisableEncodeUrlFilter、ForceEagerSessionCreationFilter。第一个不多说,第二个是禁止重定向的url编码的,第三个是为了确保session存在。

总结

  1. SessionManagementFilter的核心是SessionAuthenticationStrategy,主要涉及的主要有两个功能:Session的创建策略,Session的并发控制。
  2. SessionManagementConfigurer配置session并发控制时,会额外引入一个ConcurrentSessionFilter,用于控制session数量。由此可见,并发控制是我们遇到的第一个需要多个过滤器协作完成的功能
  3. SessionManagementFilter除了管理SessionAuthenticationStrategy之外,还有负责session超时的处理。
  4. SessionManagementFilter可以是UsernamePasswordAuthenticationFilter的补充。一来可以应付session超时,二来在发生并发请求时,可以通过SessionAuthenticationStrategy执行session相关操作。如果是其他登录方式,他就是名副其实的session管理器,离了他session相关的都不会执行。

下期预告:登录了、看了session了,下一个就是认证信息的存储。

PS:我们的花了大部分篇幅都在聊组件、配置。像SessionManagementFilter、ConcurrentSessionFilter的源码我们都没有仔细分析,原因是其核心逻辑比较简单,大家可以自行阅读。我们重点聊的还是组件功能和配置,这是spring security的学习起来比较困难的地方。

参考

Authentication Persistence and Session Management
Spring Security 如何防止 Session Fixation 攻击

相关推荐
萧瑟余晖1 小时前
JDK 26 新特性详解
java·开发语言
马优晨1 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
维天说4 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19945 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
噢,我明白了6 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品6 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘6 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记6 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端
糖果店的幽灵6 小时前
langgraph的 MessagesState 解读
java·开发语言·人工智能·windows·langgraph
码云数智-大飞7 小时前
PHP 接口开发规范:统一返回格式、异常处理与参数校验
开发语言·php