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 相关的部分(如 HttpSessionSecurityContextRepository
、SessionManagementFilter
和 RequestCacheFilter
)。
这些严格控制的直接后果是不使用 Cookie,因此每个请求都需要重新认证。这种无状态架构非常适合 REST API 及其无状态约束,也与 Basic/Digest 认证等机制配合良好。
3. 底层原理
在执行认证流程前,Spring Security 会运行一个过滤器(SecurityContextPersistenceFilter
),负责在请求间存储安全上下文。
默认情况下,上下文通过 HttpSessionSecurityContextRepository
策略存储,该策略使用 HTTP Session 作为存储介质。
对于严格的 create-session="stateless"
配置,该策略会被替换为 NullSecurityContextRepository
,此时不会创建或使用 Session 来保存上下文。
4. 并发会话控制
当已认证用户再次尝试认证时,应用程序可通过以下方式处理:
- 使其现有会话失效,并用新会话重新认证
- 允许多个会话并存
启用并发会话控制的第一步是在 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
:创建干净会话,不复制旧会话属性
8. 安全的会话 Cookie
接下来讨论如何保护会话 Cookie。
可通过 httpOnly
和 secure
标志保护会话 Cookie:
httpOnly
:若为true
,浏览器脚本将无法访问 Cookiesecure
:若为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-only
为 true
,secure
为 false
。
对应的 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 的项目,应可直接导入运行。