Spring Security 控制 Session

1、概览

本文将带你了解如何在 Spring Security 中配置 Session 超时、Session 并发以及其他高级的 Session 安全设置。

2、何时创建 Session?

可以精确控制 Session 的创建时间,以及 Spring Security 与 Session 的交互方式:

  • always:如果 Session 不存在,则会创建一个 Session。
  • ifRequired:仅在需要时才创建 Session(默认值)。
  • never:框架不会自己创建 Session,但如果 Session 已经存在,则会使用该 Session。
  • stateless:Spring Security 不会创建或使用 Session。
<http create-session="ifRequired">...</http>

Java 配置如下:

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

注意,该配置只控制 Spring Security 的行为,而不是整个应用。如果指定 Spring Security 不创建 Session,它就不会创建 Session,但是应用可能会创建 Session!

默认情况下,Spring Security 会在需要时创建一个 Session,即 ifRequired

对于无状态应用,never 选项将确保 Spring Security 本身不会创建任何 Session。但如果应用创建了 Session,Spring Security 就会使用它。

最后,最严格的 Session 创建选项 stateless(无状态)保证应用根本不会创建任何 Session。

这是在 Spring 3.1 中 引入 的,这会跳过 Spring Security Filter Chain 的部分内容,主要是 Session 相关部分,如 HttpSessionSecurityContextRepositorySessionManagementFilterRequestCacheFilter

这些更严格的控制机制直接意味着不使用 Cookie,因此每个请求都需要重新进行身份认证。

这种无状态架构与 REST API 及其无状态约束配合得很好。它们还能很好地与 BasicDigest Authentication 等身份认证机制配合使用。

3、实现细节

在运行身份认证流程之前,Spring Security 会运行一个 Filter,负责在请求之间存储 Security Context。这就是 SecurityContextPersistenceFilter

默认情况下,Context 将根据 HttpSessionSecurityContextRepository 策略进行存储,该策略使用 HTTP Session 作为存储介质。

对于严格的 create-session="stateless" 属性,将使用另一种策略 - NullSecurityContextRepository,不会创建或使用 Session 来保存 Context。

4、并发 Session 控制

当一个已通过身份认证的用户 再次尝试进行身份认证 时,应用可以通过几种方式之一来处理该事件。它可以使用户的活动的 Session 失效,然后用新的 Session 再次对用户进行身份认证,或者允许两个 Session 同时存在。

启用并发 Session 控制支持的第一步是在 web.xml 中添加以下 Listener(监听器):

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

或者,可以把它定义为一个 Bean:

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

这是确保在 Session 销毁时通知 Spring Security Session 注册表的关键。

要允许同一用户同时有多个 Session,可以在 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、Session 超时

5.1、处理 Session 超时

Session 超时后,如果用户使用过期的 Session ID 发送请求,会被重定向到一个可通过命名空间配置的 URL:

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

同样,如果用户使用未过期但完全无效的 Session ID 发送请求,也会被重定向到一个可配置的 URL:

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

下面是相应的 Java 配置:

http.sessionManagement()
  .expiredUrl("/sessionExpired.html")
  .invalidSessionUrl("/invalidSession.html");

5.2、在 Spring Boot 中配置 Session 超时

Spring Boot 中可以通过 application.properties 配置嵌入式服务器的 Session 超时时间:

server.servlet.session.timeout=15m

如果不指定时间单位,默认为秒。

简而言之,使用此配置后,Session 将在 15 分钟未活动后过期。过了这段时间,Session 将被视为无效。

注意,如果使用的是 Tomcat 服务器,它只支持 分钟粒度 的会话超时配置,最小为一分钟。这意味着,如果指定 170 秒 的超时值,实际上的超时时间为 2 分钟

最后,需要注意的是,Spring Session 也支持类似的属性(spring.session.timeout)来实现相同的目的,如果未指定该属性,自动配置将回退到先前提到的属性的值。

6、防止使用 URL 参数进行会话跟踪

在 URL 中暴露 Session 信息是一个日益严重的 安全风险

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

另外,从 Servlet 3.0 开始,也可以在 web.xml 中配置 Session 跟踪机制:

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

编程式设置如下:

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

这选择的是 JSESSIONID 的存储位置 - Cookie 或 URL 参数。

7、使用 Spring Security 进行 Session Fixation 保护

框架通过配置用户尝试重新认证时对现有 Session 的处理方式,提供了对典型 Session Fixation 攻击的保护。

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

对应的 Java 配置如下:

http.sessionManagement()
  .sessionFixation().migrateSession()

默认情况下,Spring Security 启用了这种保护(migrateSession)。身份认证时,会创建一个新的 HTTP Session,旧的 Session 会失效,旧 Session 中的属性(attribute)会被复制过来。

如果这不是想要的,还有另外两种选择:

  • 设置为 none 时,原始 Session 不会失效。
  • 设置 newSession 时,会创建一个干净的 Session,不会复制旧 Session 的任何属性。

可以使用 httpOnlysecure 标志来确保 Session Cookie 的安全:

  • httpOnly:如果为 true,则浏览器脚本(Javascript)将无法访问 Cookie
  • secure:如果为 true,则 Cookie 只能通过 HTTPS 连接发送

可以在 web.xml 中为 Session 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-onlytrue``,securefalse

相应的 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、使用 Session

9.1、Session Scope 的 Bean

只需在 Web Context 中声明的 Bean 上使用 @Scope 注解,就可以用 session 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、将原始 Session 注入 Controller

原始 HTTP Session 可以直接注入 Controller 方法:

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

9.3、获取原始 Session

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

ServletRequestAttributes attr = (ServletRequestAttributes) 
    RequestContextHolder.currentRequestAttributes();
HttpSession session= attr.getRequest().getSession(true); // true,如果 Session 不存在则自动创建

10、总结

本文介绍了如何使用 Spring Security 来管理 Http Session,包括并发设置、超时设置以及其他的高级安全设置等。


Ref:https://www.baeldung.com/spring-security-session