前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 6.x 一文讲透Session认证管理机制

Spring Security 6.x 一文讲透Session认证管理机制

原创
作者头像
fullstackyang
修改2024-06-27 22:37:58
1670
修改2024-06-27 22:37:58
举报

之前几篇文章,主要围绕着身份认证的相关内容,今天主要讨论一下认证状态的保持,由于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框架封装完成,对开发者来说并不需要太多的开发成本。整个流程大致用一个简单的时序图表示一下:

概括地说,主要有以下几步:

  1. 通常在每一种认证机制的具体实现中(图中用AbstractAuthenticationProcessingFilter表示),每当认证成功后,会新建一个SecurityContext对象,并设置已认证的Authentication对象
  2. 通过SecurityContextRepository将SecurityContext保存在session的属性中,securityContextRepository接口有多个实现类,下文作进一步介绍
  3. 将该session的sessionid写入到Cookie中
  4. 后续发起的请求再次发起访问时就会携带这个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的一个标记对象,它有以下几个成员变量
代码语言:java
复制
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腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Tomcat中Session的底层实现
  • 二、SecurityContext存取的基本流程
    • 2.1 保存SecurityContext
      • 2.2 加载SecurityContext
        • 2.3 新版本变化
        • 三、Session管理机制
          • 3.1 核心组件
            • 接口
            • 与Session管理相关的过滤器
          • 3.2 Session并发控制
            • 3.3 Session注册
              • 3.4 Session固定攻击保护
              • 四、总结
              相关产品与服务
              容器服务
              腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档