之前几篇文章,主要围绕着身份认证的相关内容,今天主要讨论一下认证状态的保持,由于HTTP协议是无状态的,因此在认证成功之后,为了让后续的请求可以继续保持住这个认证状态,避免每次请求都要重新发起认证过程,就需要对认证结果进行持久化,然后在新的请求到达时查询并还原回来对应的认证状态,通常有两种实现方案,一种是经典的cookie-session方案,即在服务端的session属性中存取认证信息,优点是实现方法比较简单,另一种是token令牌方案,利用一些算法对认证信息进行编码和解码,优点是无需落地,有效地减轻服务端存储的压力,本文主要介绍Spring Security框架中基于session的认证及常用的管理机制。
一、Tomcat中Session的底层实现
为了更好地理解session的工作方式,有必要先回顾一下session的一些背景知识,下面以Tomcat为例,大致介绍一下Session是如何在服务端维护的。
说明:下面出现的Session是Tomcat内定义的一个接口,而我们通常所说的Session,是jakarta.servlet.http(或java.servlet.http)中定义的HttpSession接口,在Tomcat中它们有一个共同的实现类为StandardSession,通过门面模式,最终实际操作的都是StandardSession对象。
在Tomcat中,主要由ManagerBase负责维护Session对象,源码如下,可以看到,session对象其实在保存在一个ConcurrentHashMap中,其Key为sessionId,根据请求携带的Cookie中JSESSIONID的值,就可以通过findSession方法查询到对应的session对象。
代码语言:java复制public abstract class ManagerBase extends LifecycleMBeanBase implements Manager {
...
protected Map<String,Session> sessions = new ConcurrentHashMap<>();
...
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
return sessions.get(id);
}
@Override
public void add(Session session) {
sessions.put(session.getIdInternal(), session);
int size = getActiveSessions();
if (size > maxActive) {
synchronized (maxActiveUpdateLock) {
if (size > maxActive) {
maxActive = size;
}
}
}
}
@Override
public Session createSession(String sessionId) {
if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) {
rejectedSessions ;
throw new TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"), maxActiveSessions);
}
// Recycle or create a Session instance
Session session = createEmptySession();
// Initialize the properties of the new session and return it
session.setNew(true);
session.setValid(true);
session.setCreationTime(System.currentTimeMillis());
session.setMaxInactiveInterval(getContext().getSessionTimeout() * 60);
String id = sessionId;
if (id == null) {
id = generateSessionId();
}
session.setId(id); // 该方法内,会调用上面的add方法,将该session保存到ConcurrentHashMap中
SessionTiming timing = new SessionTiming(session.getCreationTime(), 0);
synchronized (sessionCreationTiming) {
sessionCreationTiming.add(timing);
sessionCreationTiming.poll();
}
return session;
}
}
另外,createSession方法可以生成一个新的session对象,其中会调用session#setId方法,此时会将该session保存到ConcurrentHashMap中,该方法主要用于HttpServleRequest获取当前session的场景,可以再看一下HttpServleRequest在Tomcat中的实现类Request,当调用Request#doGetSession方法时,若当前未能查询到session对象,就会调用createSession方法,创建出一个新的session,然后再创建出一个对应的Cookie,其默认名称为“JSESSIONID”,并添加到Response中,最后返回session对象。
代码语言:java复制@Override
public HttpSession getSession(boolean create) {
Session session = doGetSession(create);
if (session == null) {
return null;
}
return session.getSession(); // 门面模式,把session自身封装到StandardSessionFacade对象中返回,StandardSessionFacade是HttpSession接口在Tomcat中的实现类
}
protected Session doGetSession(boolean create) {
Context context = getContext();
...
Manager manager = context.getManager();
...
if (requestedSessionId != null) { // 从Cookie JSESSIONID中解析出来的sessionId,也有可能为null
try {
session = manager.findSession(requestedSessionId); // 在manager中的ConcurrentHashMap查找
} catch (IOException e) {
...
}
...
if (session != null) {
session.access(); // 记录该session的访问次数
return session;
}
}
// Create a new session if requested and the response is not committed
if (!create) {
return null;
}
...
String sessionId = getRequestedSessionId();
...
session = manager.createSession(sessionId); // 创建新的Session
// Creating a new session cookie based on that session
if (session != null && trackModesIncludesCookie) {
Cookie cookie =
ApplicationSessionCookieConfig.createSessionCookie(context, session.getIdInternal(), isSecure()); // 这里会创建一个名为"JSESSIONID"的Cookie
response.addSessionCookieInternal(cookie);
}
if (session == null) {
return null;
}
session.access();
return session;
}
以上就是在tomcat中创建和读取Session的底层实现,限于篇幅,其他与Session相关方法,这里就不再展开介绍。
二、SecurityContext存取的基本流程
首先简单介绍一下如何通过Session机制保存和读取SecurityContext对象,实际上整个流程基本由Spring Security框架封装完成,对开发者来说并不需要太多的开发成本。整个流程大致用一个简单的时序图表示一下:
概括地说,主要有以下几步:
- 通常在每一种认证机制的具体实现中(图中用AbstractAuthenticationProcessingFilter表示),每当认证成功后,会新建一个SecurityContext对象,并设置已认证的Authentication对象
- 通过SecurityContextRepository将SecurityContext保存在session的属性中,securityContextRepository接口有多个实现类,下文作进一步介绍
- 将该session的sessionid写入到Cookie中
- 后续发起的请求再次发起访问时就会携带这个Cookie,在SecurityContextHolderFilter中,也是利用SecurityContextRepository得到对应session对象,进而从session的属性中取出对应的SecurityContext对象,并设置到SecurityContextHolder的ThreadLocal中,以便下游其他组件获取
下面看一下具体的实现细节。
2.1 保存SecurityContext
上文提到用于保存SecurityContext的接口SecurityContextRepository,它的默认实现类为DelegatingSecurityContextRepository,其save方法在默认配置下委托给了两个对象,即HttpSessionSecurityContextRepository和RequestAttributeSecurityContextRepository,其中RequestAttributeSecurityContextRepository实际上不进行持久化,只是将SecurityContext保存在request的属性中,因此在后续的其他请求中,无法获取到SecurityContext对象,只适用于后端dispatch的场景,而HttpSessionSecurityContextRepository则主要负责使用session实现持久化,过程比较简单:首先由request#getSession方法生成一个session,然后将SecurityContext对象写入session的一个属性(SPRING_SECURITY_CONTEXT),最后会在Response中设置对应的Cookie,写入浏览器客户端。以下是HttpSessionSecurityContextRepository#saveContext方法的源码,在Spring Security的新版本中,一般不太会发生SaveContextOnUpdateOrErrorResponseWrapper对象不为空的场景(2.3小节会解释原因),实际保存SecurityContext对象的方法为setContextInSession。
代码语言:java复制@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
SaveContextOnUpdateOrErrorResponseWrapper responseWrapper = WebUtils.getNativeResponse(response,
SaveContextOnUpdateOrErrorResponseWrapper.class);
if (responseWrapper == null) {
saveContextInHttpSession(context, request); // 通常执行这个方法
return;
}
responseWrapper.saveContext(context);
}
private void saveContextInHttpSession(SecurityContext context, HttpServletRequest request) {
if (isTransient(context) || isTransient(context.getAuthentication())) {
return;
}
SecurityContext emptyContext = generateNewContext();
if (emptyContext.equals(context)) {
HttpSession session = request.getSession(false);
removeContextFromSession(context, session);
}
else {
boolean createSession = this.allowSessionCreation;
HttpSession session = request.getSession(createSession);
setContextInSession(context, session);
}
}
private void setContextInSession(SecurityContext context, HttpSession session) {
if (session != null) {
session.setAttribute(this.springSecurityContextKey, context); //将SecurityContext对象,保存在session的属性中,其中springSecurityContextKey默认值为"SPRING_SECURITY_CONTEXT_KEY"
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Stored %s to HttpSession [%s]", context, session));
}
}
}
2.2 加载SecurityContext
加载SecurityContext的过程主要在SecurityContextHolderFilter过滤器中完成,由于很多其他的Filter在执行业务逻辑时都需要依赖SecurityContext获取认证信息,因此这个Filter在整个SecurityFilterChain的排序优先级比较高,源码如下:
代码语言:java复制private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
if (request.getAttribute(FILTER_APPLIED) != null) {
chain.doFilter(request, response);
return;
}
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
Supplier<SecurityContext> deferredContext = this.securityContextRepository.loadDeferredContext(request);
try {
this.securityContextHolderStrategy.setDeferredContext(deferredContext);
chain.doFilter(request, response);
}
finally {
this.securityContextHolderStrategy.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
加载时调用securityContextRepository的loadDeferredContext方法,该方法返回一个Supplier模式的延迟访问对象(简单理解为返回了一个访问SecurityContext对象的入口,它只在需要访问SecurityContext对象时,才执行具体的逻辑,可以提升一定的效率),如果在此之前没有发起过认证流程,这里会创建一个空的SecurityContext,而如果已经认证过,则会从session的属性中获得之前保存好的SecurityContext实例。以下是HttpSessionSecurityContextRepository#loadDeferredContext方法的源码
代码语言:java复制public DeferredSecurityContext loadDeferredContext(HttpServletRequest request) {
Supplier<SecurityContext> supplier = () -> readSecurityContextFromSession(request.getSession(false));
return new SupplierDeferredSecurityContext(supplier, this.securityContextHolderStrategy);
}
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
if (httpSession == null) {
this.logger.trace("No HttpSession currently exists");
return null;
}
// Session exists, so try to obtain a context from it.
Object contextFromSession = httpSession.getAttribute(this.springSecurityContextKey); //通过springSecurityContextKey获取session对应的属性值,并通过类型转换得到SecurityContext对象
...
// Everything OK. The only non-null return from this method.
return (SecurityContext) contextFromSession;
}
2.3 新版本变化
事实上,在Spring Security 5.7版本之前,SecurityContext的加载并不是由SecurityContextHolderFilter负责的,而是SecurityContextPersistenceFilter,他们之间有一个比较大的区别,就是SecurityContextPersistenceFilter还负责自动保存SecurityContext对象,看一下它的doFilter方法中finally代码块,这里会调用一次securityContextRepository的saveContext方法。
代码语言:java复制private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
try {
this.securityContextHolderStrategy.setContext(contextBeforeChainExecution);
...
chain.doFilter(holder.getRequest(), holder.getResponse());
}
finally {
SecurityContext contextAfterChainExecution = this.securityContextHolderStrategy.getContext();
// Crucial removal of SecurityContextHolder contents before anything else.
this.securityContextHolderStrategy.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse()); // 在SecurityContextHolderFilter中,不再自动保存SecurityContext
request.removeAttribute(FILTER_APPLIED);
this.logger.debug("Cleared SecurityContextHolder to complete request");
}
}
在旧版本中,各认证机制并不会直接操作securityContextRepository保存认证后生成的SecurityContext对象,因此在Reponse提交之前,如果本次请求执行完之后SecurityContext发生了变更,例如新设置了一个已认证的Authentication对象,那么就需要对该SecurityContext进行持久化。
举个例子,RememberMeAuthenticationFilter过滤器,用于实现“记住我”的登录机制,即通过特定cookie标识登录状态,可以在比较长一段时间内避免重新发起认证请求,Spring Security 5.6的源码如下
代码语言:java复制private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
...
Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
if (rememberMeAuth != null) {
// Attempt authentication via AuthenticationManager
try {
rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(rememberMeAuth);
SecurityContextHolder.setContext(context);
onSuccessfulAuthentication(request, response, rememberMeAuth);
// this.securityContextRepository.saveContext(context, request, response); 在Spring Security 5.7及更高版本,增加了保存SecurityContext的步骤
...
}
catch (AuthenticationException ex) {
...
}
}
chain.doFilter(request, response);
}
可以看到,在RememberMe的AuthenticationProvider认证通过之后,创建了一个新的SecurityContext,设置了Authentication对象,然后由给SecurityContextHolder托管,但其实最后并没有持久化,因此在最终Response提交之前,需要由SecurityContextPersistenceFilter统一完成持久化,这种机制看似比较合理,避免了每一种认证机制都要去操作securityContextRepository,那么新版本为何要舍弃SecurityContextPersistenceFilter这个过滤器?官方文档中,对此做了一定解释:因为对SecurityContext是否有变更,这个追踪过程是相对比较复杂的,可以翻看旧版本的源码,它借助HttpServletResponse的各种包装类,在Repsonse的各种方法内,如重定向sendRedirect,异常sendError等进行了埋点,经过很多条件的判断,才能确定是否应该保存SecurityContext,这样就会触发多次HttpSession的读写操作,但其实大部分读写操作是没有必要的,而每次请求执行完成之后,都要经历这些操作,出于效率和性能的考虑,在新版本中去除了自动保存的逻辑,使得SecurityContext的存取流程更加轻量。
三、Session管理机制
3.1 核心组件
接口
- SessionAuthenticationStrategy:只定义了一个方法,即onAuthentication,即对当前Authentication应用不同的session管理策略,它有几个常见的实现类ConcurrentSessionControlAuthenticationStrategy,ChangeSessionIdAuthenticationStrategy等(下文详述)
- sessionRegistry:主要定义了读取,新增,删除等维护SessionInfomation的方法,常用于session并发控制场景,默认实现类为SessionRegistryImpl,内部维护了两个Map,即principals和sessionIds,前者维护的是principal和sessionId的对应关系(一对多),后者维护的是sessionId和SessionInformation对象的关系(一对一)
类
- SessionInformation:它的作用相当于是Spring Security框架中用来对应HttpSession的一个标记对象,它有以下几个成员变量
public class SessionInformation implements Serializable {
...
private Date lastRequest; // 最近访问时间,每次发起请求时,如果当前Session还未失效,则刷新该时间
private final Object principal; // 标识当前用户,通常就是用户名
private final String sessionId; // 对应HttpSession的sessionId
private boolean expired = false; // 标识当前session是否过期
...
}
与Session管理相关的过滤器
- SessionManagementFilter:该过滤器是最主要负责Session管理的过滤器,上一节中提到SecurityContextHolderFilter过滤器负责将SecurityContext加载到SecurityContextHolderStrategy中,这里SessionManagementFilter则是用来SecurityContext中的Authentication对象是否已经通过认证,如果已认证,它就会调用SessionAuthenticationStrategy不同实现类对应的策略对当前session进行处理,例如session并发控制,session固定攻击等,不过SessionManagementFilter在新版本中是默认不开启的,官方文档给出的解释是每次请求都要读取session来获取SecurityContext对象,这或多或少会带来一些性能上的损耗,因此现在这项工作由认证机制本身负责完成,也就是说,session管理策略只会应用在Authentication认证通过的时候,并且仅有一次调用,这样就避免了每次请求都要读取session的问题,也属于是Spring Security框架轻量化的改进措施之一
- ConcurrentSessionFilter:该过滤器主要有两个功能,一是如果当前sessionInformation已过期,则将其清理掉,二是刷新未过期的sessionInformation的refreshLastRequest时间,则执行登出逻辑,它用于session并发控制的场景(下文详述)
- DisableEncodeUrlFilter:该过滤器是在5.7版本引入,主要用于禁止对URL重新编码,当客户端Cookie被禁用时,默认的响应就会把sessionId拼接到URL中,这样就会暴露session,存在一定的安全隐患,因此该过滤器是默认开启的。
- ForceEagerSessionCreationFilter:该过滤器是在5.7版本引入的,如果配置sessionCreationPolicy为SessionCreationPolicy.ALWAYS,它就会添加到过滤器链,且就有非常高的优先级,作用就是在请求进入过滤器链最开始的时候,就创建一个session对象,虽然这不是一种比较经济的方式,但是如果要用session来跟踪一些客户端信息时,这样做就非常有必要了
下面介绍几个比较常见的session管理场景
3.2 Session并发控制
Session并发控制,最常用的场景就是限制一个账号无法让多个客户端同时登录,即当第二个客户端发起登录,并认证通过之后,前一次认证的session就会被置为过期,用户也会被强制登出。
该配置也非常简单,即在sessionMangement DSL中的配置项sessionConcurrency配置maxinumSession为“1”。
代码语言:java复制@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http.sessionManagement(session -> session.sessionConcurrency(concurrency -> concurrency.maximumSessions(1)));
...
return http.build();
}
具体实现由策略实现类ConcurrentSessionControlAuthenticationStrategy和过滤器ConcurrentSessionFilter配合完成,首先在ConcurrentSessionControlAuthenticationStrategy中,根据规则将某些不符合要求的session置为过期,保留下合法的session,从而在ConcurrentSessionFilter中将其清理,或者刷新refreshLastRequest时间。
先来看一下ConcurrentSessionControlAuthenticationStrategy#onAuthentication方法的源码,其中allowedSessions就是我们在配置中设置的maximumSessions,然后通过sessionRegistry获取当前用户所有的sessionInformation对象,并统计其数量,当超过指定的maximumSessions时,则调用allowableSessionsExceeded方法,它会根据LastRequest的时间进行排序,最终,超过maximumSessions数量部分的,且较早时间的那些session将会被置为过期。
代码语言:java复制public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
int allowedSessions = getMaximumSessionsForThisUser(authentication);
if (allowedSessions == -1) { // maximumSessions为-1时,则表示不限制session数量
// We permit unlimited logins
return;
}
List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
int sessionCount = sessions.size();
if (sessionCount < allowedSessions) {
// They haven't got too many login sessions running at present
return;
}
if (sessionCount == allowedSessions) { // 如果该用户所有的sessionInformation数量恰好等于maximumSessions,则判断当前session的id是否包含在这些sessionInformation,如没有,则表示该session需要被失效
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, 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"));
}
// 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();
}
}
而在ConcurrentSessionFilter中,会查询出当前sessionId对应的SessionInformation对象,判断是否已标记为过期,若未过期,则调用sessionRegistry#refreshLastRequest刷新时间,若已过期,则调用登出逻辑,包括将HttpSession对象置为失效,清空securityContextRepository中的SecurityContext对象等,因此它在SecurityFilterChain的优先级通常排在SessionManagementFilter(早期版本)以及各种认证机制对应的Filter(如AbstractAuthenticationProcessingFilter的实现类)之后,以下是doFilter方法源码
代码语言:java复制private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) {
SessionInformation info = this.sessionRegistry.getSessionInformation(session.getId());
if (info != null) {
if (info.isExpired()) {
// Expired - abort processing
this.logger.debug(LogMessage
.of(() -> "Requested session ID " request.getRequestedSessionId() " has expired."));
doLogout(request, response); // sessionInformation被标记为已过期,执行登出逻辑,以及过期策略
this.sessionInformationExpiredStrategy
.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response)); //默认实现为ResponseBodySessionInformationExpiredStrategy,它向Response写了一段告知用户session已过期的文字
return;
}
// Non-expired - update last request date/time
this.sessionRegistry.refreshLastRequest(info.getSessionId()); // 对于未过期的session,则更新其lastRequest时间
}
}
chain.doFilter(request, response);
}
3.3 Session注册
为了让ConcurrentSessionControlAuthenticationStrategy在清理多余session时,能够快速获得该用户下所有的session,就需要在每次认证通过后注册新的session信息,因此RegisterSessionAuthenticationStrategy经常与上一节提到的ConcurrentSessionControlAuthenticationStrategy配套使用。
代码语言:java复制public class RegisterSessionAuthenticationStrategy implements SessionAuthenticationStrategy {
private final SessionRegistry sessionRegistry;
...
@Override
public void onAuthentication(Authentication authentication, HttpServletRequest request,
HttpServletResponse response) {
this.sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
}
}
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {
...
private final ConcurrentMap<Object, Set<String>> principals;
private final Map<String, SessionInformation> sessionIds;
...
@Override
public void registerNewSession(String sessionId, Object principal) {
...
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
...
this.sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
this.principals.compute(principal, (key, sessionsUsedByPrincipal) -> {
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = new CopyOnWriteArraySet<>();
}
sessionsUsedByPrincipal.add(sessionId);
this.logger.trace(LogMessage.format("Sessions used by '%s' : %s", principal, sessionsUsedByPrincipal));
return sessionsUsedByPrincipal;
});
}
}
注册时,若原sessionId有对应的SessionInformation对象,先将其清理掉,然后创建一个新的SessionInformation对象,将sessionId和SessionInformation对象的关系写入sessionIds的Map,将principal和sessionId的关系写入principals的Map。
3.4 Session固定攻击保护
所谓Session固定攻击,主要是指用户登录前和登录后所使用的session保持不变,这样攻击者可以事先准备好一个session,然后诱导用户使用该session进行登录,最后攻击者就可以使用这个session成功冒充该用户进入系统。
在Spring Security提供3种可配置的Session固定攻击保护策略,即changeSessionId、newSession和migrateSession(配置如下),其中changeSessionId,对应ChangeSessionIdAuthenticationStrategy实现类,newSession和migrateSession,对应SessionFixationProtectionStrategy实现类,前者不需要创建新的session,后面两个都会创建新的session,区别在于newSession不会保留原session的属性值(仅保留Spring Security自己定义的属性),而migrateSession则会迁移原session属性。
代码语言:java复制@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http.sessionManagement(session -> session.sessionFixation(fixation -> fixation.changeSessionId()));
http.sessionManagement(session -> session.sessionFixation(fixation -> fixation.newSession()));
http.sessionManagement(session -> session.sessionFixation(fixation -> fixation.migrateSession()));
...
return http.build();
}
下面分别介绍一下两个实现类的具体细节。
ChangeSessionIdAuthenticationStrategy的实现最为简单,源码如下,它利用了HttpServletRequest#changeSessionId方法,给当前Session赋予一个新的sessionId,这时会对Tomcat中ManagerBase维护的ConcurrentHashMap进行更新,删除旧sessionId,添加新sessionId作为key,这样就无法使用旧的sessionId查询到session对象了,这种实现方式比较轻量,不过也依赖底层容器的支持,因此只能在 Servlet 3.1及更新版本的容器中使用,同时也是默认的实现类。
代码语言:java复制public final class ChangeSessionIdAuthenticationStrategy extends AbstractSessionFixationProtectionStrategy {
@Override
HttpSession applySessionFixation(HttpServletRequest request) {
request.changeSessionId();
return request.getSession();
}
}
SessionFixationProtectionStrategy的实现则稍微复杂一点,它相当于给session做了一次迁移工作:首先把原session的属性暂存到一个Map中(上文提到,当选择newSession时,仅保留Spring Security自己定义的属性),然后把旧的session被置为失效,并创建出一个新的Session,最后之前暂存在Map中的属性都迁移进去,这种的实现方式比较重,因此它只是在Servlet3.0及更早版本的容器中作为默认实现。
代码语言:java复制final HttpSession applySessionFixation(HttpServletRequest request) {
HttpSession session = request.getSession();
String originalSessionId = session.getId();
this.logger.debug(LogMessage.of(() -> "Invalidating session with Id '" originalSessionId "' "
(this.migrateSessionAttributes ? "and" : "without") " migrating attributes."));
Map<String, Object> attributesToMigrate = extractAttributes(session);
int maxInactiveIntervalToMigrate = session.getMaxInactiveInterval();
session.invalidate();
session = request.getSession(true); // we now have a new session
this.logger.debug(LogMessage.format("Started new session: %s", session.getId()));
transferAttributes(attributesToMigrate, session);
if (this.migrateSessionAttributes) {
session.setMaxInactiveInterval(maxInactiveIntervalToMigrate);
}
return session;
}
四、总结
本文就session的底层实现,SecurityContext在Session中的存取流程,以及常用的Session管理场景做了相关介绍,最后,再做一个总结:
- session是存储在服务端的一个对象,在生成session对象时,会添加一个Cookie到Response中,cookie的值即为sessionId
- 在Tomcat中,由ManagerBase负责维护session对象,内部定义了一个ConcurrentHashMap的变量sessions,其key为sessionId,用以存储和查询session对象
- 在新版本Spring Security中,出于性能等方面的考虑,SecurityContextPersistenceFilter已经默认不生效,取而代之的是SecurityContextHolderFilter,它主要负责通过securityContextRepository加载已认证的SecurityContext对象到securityContextHolderStrategy中
- 存储SecurityContext的工作由每个认证机制的实现类负责,具体执行存储逻辑在HttpSessionSecurityContextRepository中,保存SecurityContext对象,即写入session的一个属性值“SPRING_SECURITY_CONTEXT”
- Spring Security框架提供若干session管理的配置,常见的有session并发控制,session固定攻击
- session并发控制主要由策略实现类ConcurrentSessionControlAuthenticationStrategy和过滤器ConcurrentSessionFilter配合完成,前者负责将不符合规则的session标记为过期,后者负责清理掉这些过期session
- session固定攻击保护有三种对应的配置changeSessionId、newSession和migrateSession,其中changeSessionId主要由ChangeSessionIdAuthenticationStrategy实现,后两种由SessionFixationProtectionStrategy实现,ChangeSessionIdAuthenticationStrategy实现比较轻量简单,即生成一个新的sessionId,赋予当前session,SessionFixationProtectionStrategy实现相对复杂,需要生成一个新的session,然后将原session的属性迁移过去。
我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!