前言
在聊认证过滤器的时候,我们埋了个坑: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的安全威胁
-
会话劫持(Session Hijacking)
攻击者通过各种手段(如网络嗅探、跨站脚本攻击等)获取到用户的Session ID,然后利用这个有效的Session ID伪装成合法用户,访问用户的敏感信息或执行恶意操作。
-
会话固定(Session Fixation)
攻击者预先在用户的目标服务器上创建一个有效的Session,然后通过各种手段诱使用户使用这个Session。这样,攻击者就可以在用户正常登录后接管用户的会话,获取用户的敏感信息。
-
跨站请求伪造(CSRF)
攻击者诱导用户访问一个恶意网站,该网站包含指向目标网站的恶意请求,由于用户的浏览器会自动带上用户的Session信息,因此这个恶意请求会被视为用户的合法请求,从而导致用户的数据被篡改或泄露。
-
中间人攻击
攻击者通过拦截客户端和服务器之间的通信,修改或窃取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协作的过滤器。但这里我们只聊他在这里的作用,聊到跨域请求防御再把这块串起来。
javapublic 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进行统计,并以此决定后续操作。
javapublic 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
javapublic 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超时的请求如果想要处理,例如跳转登录页面,就得我们自己处理了。
而像AbstractPreAuthenticatedProcessingFilter
、RememberMeAuthenticationFilter
、BasicAuthenticationFilter
等,就不会执行这个。甚至,也不会执行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存在。
总结
- SessionManagementFilter的核心是SessionAuthenticationStrategy,主要涉及的主要有两个功能:Session的创建策略,Session的并发控制。
- SessionManagementConfigurer配置session并发控制时,会额外引入一个ConcurrentSessionFilter,用于控制session数量。由此可见,并发控制是我们遇到的第一个需要多个过滤器协作完成的功能。
- SessionManagementFilter除了管理SessionAuthenticationStrategy之外,还有负责session超时的处理。
- SessionManagementFilter可以是UsernamePasswordAuthenticationFilter的补充。一来可以应付session超时,二来在发生并发请求时,可以通过SessionAuthenticationStrategy执行session相关操作。如果是其他登录方式,他就是名副其实的session管理器,离了他session相关的都不会执行。
下期预告:登录了、看了session了,下一个就是认证信息的存储。
PS:我们的花了大部分篇幅都在聊组件、配置。像SessionManagementFilter、ConcurrentSessionFilter的源码我们都没有仔细分析,原因是其核心逻辑比较简单,大家可以自行阅读。我们重点聊的还是组件功能和配置,这是spring security的学习起来比较困难的地方。
参考
Authentication Persistence and Session Management
Spring Security 如何防止 Session Fixation 攻击