1. 概述

本教程将演示 Spring Security 如何帮助我们控制 HTTP Session 的行为。这些控制包括会话超时设置、启用并发会话以及其他高级安全配置。

2. Session 何时创建?

我们可以精确控制 Session 的创建时机以及 Spring Security 与其交互的方式:

  • always:如果 Session 不存在,则始终创建
  • ifRequired:仅在需要时创建(默认值
  • never:框架本身不会创建 Session,但会使用已存在的 Session
  • stateless:Spring Security 不会创建或使用任何 Session
<http create-session="ifRequired">...</http>

对应的 Java 配置:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(httpSecuritySessionManagementConfigurer -> 
               httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
    return http.build();
}

⚠️ 关键理解:此配置仅控制 Spring Security 的行为,而非整个应用程序。即使我们禁止 Spring Security 创建 Session,应用程序本身仍可能创建!

默认情况下,Spring Security 在需要时会创建 Session(即 ifRequired 模式)。

对于更偏向无状态的应用never 选项可确保 Spring Security 自身不创建 Session。但如果应用程序创建了 Session,Spring Security 仍会使用它。

最严格的选项 stateless保证应用程序完全不创建 Session。该选项自 Spring 3.1 引入,会跳过 Spring Security 过滤链中与 Session 相关的部分(如 HttpSessionSecurityContextRepositorySessionManagementFilterRequestCacheFilter)。

这些严格控制的直接后果是不使用 Cookie,因此每个请求都需要重新认证。这种无状态架构非常适合 REST API 及其无状态约束,也与 Basic/Digest 认证等机制配合良好。

3. 底层原理

在执行认证流程前,Spring Security 会运行一个过滤器(SecurityContextPersistenceFilter),负责在请求间存储安全上下文。

默认情况下,上下文通过 HttpSessionSecurityContextRepository 策略存储,该策略使用 HTTP Session 作为存储介质。

对于严格的 create-session="stateless" 配置,该策略会被替换为 NullSecurityContextRepository,此时不会创建或使用 Session 来保存上下文。

4. 并发会话控制

当已认证用户再次尝试认证时,应用程序可通过以下方式处理:

  1. 使其现有会话失效,并用新会话重新认证
  2. 允许多个会话并存

启用并发会话控制的第一步是在 web.xml 中添加监听器:

<listener>
    <listener-class>
      org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
</listener>

或定义为 Bean:

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

✅ 此步骤至关重要,确保在 Session 销毁时通知 Spring Security 的会话注册表。

要允许同一用户拥有多个并发会话,需在 XML 配置中使用 <session-management> 元素:

<http ...>
    <session-management>
        <concurrency-control max-sessions="2" />
    </session-management>
</http>

对应的 Java 配置:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.sessionManagement().maximumSessions(2)
}

5. 会话超时

5.1. 处理会话超时

当会话超时后,若用户使用已过期的会话 ID 发送请求,将被重定向到可配置的 URL:

<session-management>
    <concurrency-control expired-url="/sessionExpired.html" ... />
</session-management>

类似地,若用户使用未过期但完全无效的会话 ID 发送请求,也会被重定向:

<session-management invalid-session-url="/invalidSession.html">
    ...
</session-management>

对应的 Java 配置:

http.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer
  .expiredUrl("/sessionExpired")
  .invalidSessionUrl("/invalidSession"));

5.2. 通过 Spring Boot 配置会话超时

使用属性可轻松配置嵌入式服务器的会话超时值:

server.servlet.session.timeout=15m

若未指定时间单位,Spring 默认使用秒。

简单来说,此配置会使会话在 15 分钟无活动后过期,之后该会话被视为无效。

若项目使用 Tomcat,需注意其仅支持分钟精度的会话超时(最短 1 分钟)。例如设置 170s 会被转换为 2 分钟超时。

最后需说明,即使 Spring Session 提供了类似属性(spring.session.timeout),若未指定,自动配置将回退到我们首先提到的属性值。

6. 禁止使用 URL 参数进行会话跟踪

在 URL 中暴露会话信息是日益严重的安全风险(在 OWASP Top 10 中从 2007 年的第 7 位升至 2013 年的第 2 位)。

自 Spring 3.0 起,可通过在 <http> 命名空间中设置 disable-url-rewriting="true" 来禁用将 jsessionid 追加到 URL 的重写逻辑。

或者,自 Servlet 3.0 起,可在 web.xml 中配置会话跟踪机制:

<session-config>
     <tracking-mode>COOKIE</tracking-mode>
</session-config>

或通过编程方式:

servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE));

此配置决定 JSESSIONID 的存储位置:Cookie 或 URL 参数。

7. Spring Security 的会话固定保护

框架通过配置用户再次认证时对现有会话的处理方式,提供针对典型会话固定攻击的保护:

<session-management session-fixation-protection="migrateSession"> ...

对应的 Java 配置:

http.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionFixation().migrateSession());

默认情况下,Spring Security 启用此保护(migrateSession 模式)。认证时会创建新 HTTP Session,使旧会话失效,并将旧会话属性复制到新会话。

若不符合需求,还有两个选项:

  • none:保留原始会话
  • newSession:创建干净会话,不复制旧会话属性

接下来讨论如何保护会话 Cookie。

可通过 httpOnlysecure 标志保护会话 Cookie

  • httpOnly:若为 true,浏览器脚本将无法访问 Cookie
  • secure:若为 true,Cookie 仅通过 HTTPS 连接发送

web.xml 中为会话 Cookie 设置这些标志:

<session-config>
    <session-timeout>1</session-timeout>
    <cookie-config>
        <http-only>true</http-only>
        <secure>true</secure>
    </cookie-config>
</session-config>

此配置选项自 Java Servlet 3 起可用。默认情况下,http-onlytruesecurefalse

对应的 Java 配置:

public class MainWebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext sc) throws ServletException {
        // ...
        sc.getSessionCookieConfig().setHttpOnly(true);        
        sc.getSessionCookieConfig().setSecure(true);        
    }
}

若使用 Spring Boot,可在 application.properties 中设置

server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

也可通过 Filter 手动实现:

public class SessionFilter implements Filter {
    @Override
    public void doFilter(
      ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        Cookie[] allCookies = req.getCookies();
        if (allCookies != null) {
            Cookie session = 
              Arrays.stream(allCookies).filter(x -> x.getName().equals("JSESSIONID"))
                    .findFirst().orElse(null);

            if (session != null) {
                session.setHttpOnly(true);
                session.setSecure(true);
                res.addCookie(session);
            }
        }
        chain.doFilter(req, res);
    }
}

9. 操作会话

9.1. 会话作用域 Bean

通过在 Web 上下文中声明的 Bean 上使用 @Scope 注解,可定义会话作用域的 Bean:

@Component
@Scope("session")
public class Foo { .. }

或使用 XML:

<bean id="foo" scope="session"/>

然后可将该 Bean 注入到其他 Bean 中:

@Autowired
private Foo theFoo;

Spring 会将新 Bean 绑定到 HTTP Session 的生命周期。

9.2. 将原始会话注入控制器

原始 HTTP Session 可直接注入到控制器方法中:

@RequestMapping(..)
public void fooMethod(HttpSession session) {
    session.setAttribute(Constants.FOO, new Foo());
    //...
    Foo foo = (Foo) session.getAttribute(Constants.FOO);
}

9.3. 获取原始会话

当前 HTTP Session 也可通过原始 Servlet API 编程获取:

ServletRequestAttributes attr = (ServletRequestAttributes) 
    RequestContextHolder.currentRequestAttributes();
HttpSession session= attr.getRequest().getSession(true); // true == 允许创建

10. 总结

本文讨论了使用 Spring Security 管理 Session 的方法。Spring 参考文档中包含非常详尽的会话管理 FAQ

本文中的代码示例可在 GitHub 获取。这是一个基于 Maven 的项目,应可直接导入运行。


原始标题:Control the Session with Spring Security