SpringSecurity中文文档(Servlet Session Management)

Authentication Persistence and Session Management

一旦您拥有了正在对请求进行身份验证的应用程序,就必须考虑如何在将来的请求中持久化和恢复结果身份验证。

默认情况下,这是自动完成的,因此不需要额外的代码,尽管了解 requireExplicitSave在 HttpSecurity 中的含义非常重要。

如果您愿意,您可以阅读更多关于 RequureExplicSave 正在做什么requireExplicitSave is doing 或者为什么它很重要why it's important的内容。否则,在大多数情况下您将完成本节。

但是在离开之前,考虑一下这些用例是否适合您的应用程序:

Understanding Session Management's Components

会话管理支持由几个组件组成,它们共同提供功能。

这些组件是 SecurityContextHolderFilterSecurityContextPersistenceFilterSessionManagementFilter

在 SpringSecurity6中,默认情况下不设置 SecurityContextPersisenceFilter 和 SessionManagementFilter。除此之外,任何应用程序都应该只设置 SecurityContextHolderFilter 或 SecurityContextPersisenceFilter,而不能同时设置两者。

The SessionManagementFilter

SessionManagementFilter 根据 SecurityContextHolder 的当前内容检查 SecurityContextRepository 的内容,以确定用户在当前请求期间是否已被身份验证,通常是通过非交互式身份验证机制,如预先身份验证或 remember-me [1]。如果存储库包含安全上下文,则filter不执行任何操作。如果没有,并且线程本地的 SecurityContext 包含一个(非匿名的) Authentication 对象,则筛选器假定它们已经通过堆栈中以前的筛选器进行了身份验证。然后它将调用配置的 SessionAuthenticationStrategy。

如果用户当前没有经过身份验证,filter 将检查是否请求了无效的会话 ID (例如,由于超时) ,并在设置了一个会话 ID 的情况下调用配置的 InvalidSessionStrategy。最常见的行为就是重定向到一个固定的 URL,这封装在标准实现 SimpleRedirectInvalidSessionStrategy 中。如前所述,在通过命名空间配置无效的会话 URL 时也使用后者。

Moving Away From SessionManagementFilter

在 Spring Security 5中,默认配置依赖于 SessionManagementFilter 来检测用户是否刚刚通过身份验证并调用 SessionAuthenticationStrategy。这样做的问题在于,它意味着在典型的设置中,必须为每个请求读取 HttpSession。

在 SpringSecurity6中,默认情况是身份验证机制本身必须调用 SessionAuthenticationStrategy。这意味着不需要检测身份验证何时完成,因此不需要为每个请求读取 HttpSession。

Things To Consider When Moving Away From SessionManagementFilter

在 Spring Security 6中,默认情况下不使用 SessionManagementFilter,因此,来自 sessionManagement DSL 的一些方法不会产生任何效果。

Method Replacement
sessionAuthenticationErrorUrl Configure an AuthenticationFailureHandler in your authentication mechanism
sessionAuthenticationFailureHandler Configure an AuthenticationFailureHandler in your authentication mechanism
sessionAuthenticationStrategy Configure an SessionAuthenticationStrategy in your authentication mechanism as discussed above

如果尝试使用这些方法中的任何一种,将引发异常。

Customizing Where the Authentication Is Stored

默认情况下,SpringSecurity 在 HTTP 会话中为您存储安全上下文。然而,这里有几个你可能需要自定义的原因:

  • 您可能希望在 HttpSessionSecurityContextRepository 实例上调用个别的setters
  • 您可能希望将安全上下文存储在缓存或数据库中,以启用水平伸缩

首先,您需要创建 SecurityContextRepository 的实现,或者使用类似 HttpSessionSecurityContextRepository 的现有实现,然后您可以在 HttpSecurity 中设置它。

Customizing the SecurityContextRepository

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    SecurityContextRepository repo = new MyCustomSecurityContextRepository();
    http
        // ...
        .securityContext((context) -> context
            .securityContextRepository(repo)
        );
    return http.build();
}

上述配置设置 SecurityContextHolderFilter 上的 SecurityContextRepository 和参与身份验证过滤器,如 UsernamePasswordAuthenticationFilter。要在无状态筛选器中设置它,请参阅如何自定义 SecurityContextRepository for Statless Authentication。

如果使用自定义身份验证机制,则可能希望自己存储身份验证。

Storing the Authentication manually

例如,在某些情况下,您可能需要手动验证用户,而不是依赖于 Spring Security filters。您可以使用自定义过滤器或 Spring MVC 控制器端点来完成这项工作。如果要在请求之间保存身份验证,例如,在 HttpSession 中,必须这样做:

java 复制代码
private SecurityContextRepository securityContextRepository =
        new HttpSessionSecurityContextRepository(); // 1

@PostMapping("/login")
public void login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) { // 2
    UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
        loginRequest.getUsername(), loginRequest.getPassword()); // 3 
    Authentication authentication = authenticationManager.authenticate(token); // 4 
    SecurityContext context = securityContextHolderStrategy.createEmptyContext();
    context.setAuthentication(authentication); // 5
    securityContextHolderStrategy.setContext(context);
    securityContextRepository.saveContext(context, request, response); // 6
}

class LoginRequest {

    private String username;
    private String password;

    // getters and setters
}
  1. 将 SecurityContextRepository 添加到控制器
  2. 注入 HttpServletRequest 和 HttpServletResponse 以保存 SecurityContext
  3. 使用提供的凭据创建未经身份验证的 UsernamePasswordAuthenticationToken
  4. 调用 AuthenticationManager # authenticate 对用户进行身份验证
  5. 创建 SecurityContext 并在其中设置身份验证
  6. 在 SecurityContextRepository 中保存 SecurityContext

就是这样。如果您不确定上面示例中的 securityContextHolderStrategy 是什么,可以在使用 SecurityContextStrategy 部分了解更多信息。

Properly Clearing an Authentication

如果您正在使用 Spring Security 的 Logout Support,那么它将为您处理许多事情,包括清除和保存上下文。但是,假设您需要手动将用户从应用程序中注销。在这种情况下,您需要确保正确地清除和保存上下文。

Configuring Persistence for Stateless Authentication

例如,有时不需要创建和维护 HttpSession,以便跨请求持久化身份验证。某些身份验证机制(如 HTTPBasic)是无状态的,因此会在每个请求上重新验证用户。

如果您不希望创建会话,可以使用 SessionCreationPolicy. STATELSS,如下所示:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}

上述配置正在将 SecurityContextRepository 配置为使用 NullSecurityContextRepository,并且还阻止将请求保存到会话中。

如果您使用的是 SessionCreationPolicy. NEVER,您可能会注意到应用程序仍然在创建 HttpSession。在大多数情况下,发生这种情况是因为请求被保存在会话中,以便在身份验证成功后再次请求经过身份验证的资源。为了避免这种情况,请参考如何防止请求被保存 how to prevent the request of being saved 节。

Storing Stateless Authentication in the Session

如果出于某种原因,您正在使用无状态身份验证机制,但仍然希望在会话中存储身份验证,则可以使用 HttpSessionSecurityContextRepository 而不是 NullSecurityContextRepository。

对于 HTTP Basic,可以添加一个 ObjectPostProcessor,用于更改 BasicAuthenticationFilter 使用的 SecurityContextRepository:

Store HTTP Basic authentication in the HttpSession

java 复制代码
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        // ...
        .httpBasic((basic) -> basic
            .addObjectPostProcessor(new ObjectPostProcessor<BasicAuthenticationFilter>() {
                @Override
                public <O extends BasicAuthenticationFilter> O postProcess(O filter) {
                    filter.setSecurityContextRepository(new HttpSessionSecurityContextRepository());
                    return filter;
                }
            })
        );

    return http.build();
}

上述方法也适用于其他身份验证机制,如承载令牌身份验证。

在 Spring Security 5中,默认行为是使用 SecurityContextPersisenceFilter 将 SecurityContext 自动保存到 SecurityContextRepository。必须在提交 HttpServletResponse 之前和 SecurityContextPersisenceFilter 之前保存。不幸的是,SecurityContext 的自动持久化在请求完成之前(即在提交 HttpServletResponse 之前)完成时可能会让用户感到惊讶。跟踪状态以确定是否需要保存,从而导致有时不必要地写入 SecurityContextRepository (即 HttpSession)也很复杂。

由于这些原因,不推荐使用 SecurityContextHolderFilter 替换 SecurityContextPersisenceFilter。在 Spring Security 6中,默认行为是 SecurityContextHolderFilter 将只从 SecurityContextRepository 读取 SecurityContext 并在 SecurityContextHolder 中填充它。用户现在必须使用 SecurityContextRepository 显式地保存 SecurityContext,如果他们希望 SecurityContext 在请求之间保持的话。这样可以消除模糊性,并在必要时只需要写入 SecurityContextRepository (即 HttpSession) ,从而提高性能。

How it works

总而言之,如果 requireExplicitSave 为 true,Spring Security 将设置 SecurityContextHolderFilter 而不是 SecurityContextPersisenceFilter

Configuring Concurrent Session Control

如果您希望限制单个用户登录到您的应用程序的能力,Spring Security 通过以下简单的添加支持开箱即用。首先,您需要将以下侦听器添加到您的配置中,以保持 Spring Security 对会话生命周期事件的更新:

java 复制代码
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

然后在安全配置中添加以下代码行:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
        );
    return http.build();
}

这将阻止用户多次登录------第二次登录将导致第一次登录失效。

使用 Spring Boot,您可以通过以下方式测试上面的配置场景:

java 复制代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsTests {

    @Autowired
    private MockMvc mvc;

    @Test
    void loginOnSecondLoginThenFirstSessionTerminated() throws Exception {
        MvcResult mvcResult = this.mvc.perform(formLogin())
                .andExpect(authenticated())
                .andReturn();

        MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();

        this.mvc.perform(get("/").session(firstLoginSession))
                .andExpect(authenticated());

        this.mvc.perform(formLogin()).andExpect(authenticated());

        // first session is terminated by second login
        this.mvc.perform(get("/").session(firstLoginSession))
                .andExpect(unauthenticated());
    }

}

可以使用"最大会话"示例进行尝试。

另外,通常您希望防止第二次登录,在这种情况下,您可以使用:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
        );
    return http.build();
}

第二次登录将被拒绝。"拒绝"的意思是,如果使用的是基于表单的登录,那么用户将被发送到身份验证-失败-url。如果第二次身份验证是通过另一种非交互机制进行的,比如" remember-me",那么将向客户端发送一个"未授权"(401)错误。如果希望使用错误页面,可以将属性 session-entication-error-url 添加到session-management元素中。

使用 Spring Boot,您可以通过以下方式测试上述配置:

java 复制代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsPreventLoginTests {

    @Autowired
    private MockMvc mvc;

    @Test
    void loginOnSecondLoginThenPreventLogin() throws Exception {
        MvcResult mvcResult = this.mvc.perform(formLogin())
                .andExpect(authenticated())
                .andReturn();

        MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();

        this.mvc.perform(get("/").session(firstLoginSession))
                .andExpect(authenticated());

        // second login is prevented
        this.mvc.perform(formLogin()).andExpect(unauthenticated());

        // first session is still valid
        this.mvc.perform(get("/").session(firstLoginSession))
                .andExpect(authenticated());
    }

}

如果对基于表单的登录使用自定义身份验证filter,则必须显式配置并发会话控制支持。您可以使用"最大会话防止登录"示例尝试使用它。

Detecting Timeouts

会话会自行到期,不需要做任何事情来确保删除安全上下文。也就是说,SpringSecurity 可以检测会话何时过期,并采取您指示的特定操作。例如,当用户使用已过期的会话发出请求时,您可能希望重定向到特定的端点。这是通过 HttpSecurity 中的无效 SessionUrl 实现的:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/invalidSession")
        );
    return http.build();
}

请注意,如果使用此机制检测会话超时,则如果用户注销然后在没有关闭浏览器的情况下重新登录,则可能会错误地报告错误。这是因为当您使会话无效时,会话 cookie 不会被清除,即使用户已经登出,它也会被重新提交。如果是这种情况,您可能需要配置注销以清除会话 cookie。

Customizing the Invalid Session Strategy

ValidSessionUrl 是一种方便的方法,用于使用 SimpleRedirectInvalidSessionStrategy 实现设置 InvalidSessionStrategy。如果希望自定义行为,可以实现 InvalidSessionStrategy 接口,并使用 valididSessionStrategy 方法对其进行配置:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionStrategy(new MyCustomInvalidSessionStrategy())
        );
    return http.build();
}

Clearing Session Cookies on Logout

你可以在注销时显式地删除这个 JSESSIONID cookie,例如在注销处理程序中使用 Clear-Site-Data 头:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout((logout) -> logout
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
        );
    return http.build();
}

这样做的好处是容器不可知,并且适用于任何支持 Clear-Site-Data 报头的容器。

作为替代,您还可以在注销处理程序中使用以下语法:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID")
        );
    return http.build();
}

不幸的是,这并不能保证在每个 servlet 容器中都能正常工作,因此您需要在您的环境中对其进行测试。

Understanding Session Fixation Attack Protection

会话固定攻击是一种潜在的风险,恶意攻击者可能通过访问一个站点来创建一个会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。SpringSecurity 通过创建新会话或在用户登录时更改会话 ID 来自动防止这种情况发生。

Configuring Session Fixation Protection

你可以通过选择三个推荐的选项来控制会话固定保护策略:

  • ChangeSessionId-不要创建新的会话,而是使用 Servlet 容器(HttpServletRequest # changeSessionId ())提供的会话固定保护。此选项仅在 Servlet 3.1(JavaEE7)和更新的容器中可用。在旧容器中指定它将导致异常。这是 Servlet 3.1和更新的容器中的默认值。
  • NewSession-创建一个新的" clean"会话,不复制现有的会话数据(仍将复制与 Spring Security 相关的属性)。
  • MigateSession-创建一个新会话并将所有现有会话属性复制到新会话。这是 Servlet 3.0或更老的容器中的默认值。

您可以通过以下方法配置会话固定保护:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement((session) -> session
            .sessionFixation((sessionFixation) -> sessionFixation
                .newSession()
            )
        );
    return http.build();
}

当发生会话固定保护时,会导致在应用程序上下文中发布 SessionFixationProtectionEvent。

如果您使用 changeSessionId,这种保护也会导致任何 jakarta.servlet.http.HttpSessionIdListener 正在通知 ,因此如果您的代码同时侦听这两个事件,请谨慎使用。

您还可以将会话固定保护设置为无,以禁用它,但是不建议这样做,因为这会使您的应用程序容易受到攻击。

Using SecurityContextHolderStrategy

考虑以下代码块:

java 复制代码
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
        loginRequest.getUsername(), loginRequest.getPassword());
Authentication authentication = this.authenticationManager.authenticate(token);
// ...
SecurityContext context = SecurityContextHolder.createEmptyContext(); // 1
context.setAuthentication(authentication);// 2
SecurityContextHolder.setContext(context);// 3
  1. 通过静态访问 SecurityContextHolder 创建一个空的 SecurityContext 实例。
  2. 设置 SecurityContext 实例中的 Authentication 对象。
  3. 静态设置 SecurityContextHolder 中的 SecurityContext 实例。

虽然上面的代码工作得很好,但它可能会产生一些不想要的效果: 当组件通过 SecurityContextHolder 静态访问 SecurityContext 时,当有多个应用程序上下文需要指定 SecurityContextHolderStrategy 时,这可能会创建竞态条件。这是因为在 SecurityContextHolder 中,每个类加载器有一个策略,而不是每个应用程序上下文有一个策略。

为了解决这个问题,组件可以从应用程序上下文连接 SecurityContextHolderStrategy。默认情况下,他们仍然会从 SecurityContextHolder 查找策略。

这些变化很大程度上是内部的,但是它们为应用程序提供了自动连接 SecurityContextHolderStrategy 而不是静态访问 SecurityContext 的机会。为此,应将代码更改为:

java 复制代码
public class SomeClass {

    private final SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder.getContextHolderStrategy();

    public void someMethod() {
        UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(
                loginRequest.getUsername(), loginRequest.getPassword());
        Authentication authentication = this.authenticationManager.authenticate(token);
        // ...
        SecurityContext context = this.securityContextHolderStrategy.createEmptyContext(); // 1
        context.setAuthentication(authentication);// 2
        this.securityContextHolderStrategy.setContext(context);// 3
    }

}
  1. 使用配置的 SecurityContextHolderStrategy 创建一个空的 SecurityContext 实例。
  2. 设置 SecurityContext 实例中的 Authentication 对象。
  3. 设置 SecurityContextHolderStrategy 中的 SecurityContext 实例。

Forcing Eager Session Creation

有时,急切地创建会话可能很有价值。这可以通过使用 ForceEagerSessionCreationFilter 完成,该过滤器可以使用以下方式配置:

java 复制代码
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
        );
    return http.build();
}

延伸阅读

使用 Spring Session的集群Seession

相关推荐
Theodore_10223 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸4 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象5 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了5 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·5 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王6 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康6 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
qq_17448285756 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
转世成为计算机大神6 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式