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 攻击

相关推荐
Aileen_0v0几秒前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
桂月二二2 小时前
Java与容器化:如何使用Docker和Kubernetes优化Java应用的部署
java·docker·kubernetes
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
小马爱打代码2 小时前
设计模式详解(建造者模式)
java·设计模式·建造者模式
栗子~~3 小时前
idea 8年使用整理
java·ide·intellij-idea
2301_801483693 小时前
Maven核心概念
java·maven
007php0073 小时前
linux服务器上CentOS的yum和Ubuntu包管理工具apt区别与使用实战
linux·运维·服务器·ubuntu·centos·php·ai编程
Q_19284999063 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
我要学编程(ಥ_ಥ)4 小时前
初始JavaEE篇 —— 网络原理---传输层协议:深入理解UDP/TCP
java·网络·tcp/ip·udp·java-ee
就爱学编程4 小时前
重生之我在异世界学编程之C语言:数据在内存中的存储篇(下)
java·服务器·c语言