Authentication Persistence and Session Management

翻译版本 【spring-security 6.2.1 】session-managemen

Authentication Persistence and Session Management

一旦您获得了一个正在对请求进行身份验证的应用程序,就必须考虑如何在未来的请求中持久化和恢复所产生的身份验证。
默认情况下,这是自动完成的,所以不需要额外的代码,但重要的是要知道requireExplicitSave在HttpSecurity中的含义。

如果你喜欢,你可以阅读更多关于requireExplicitSave是做什么的,或者为什么它很重要。否则,在大多数情况下,您已经完成了这一部分。

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

  • 我想了解会话管理的组件
  • 我想限制用户可以同时登录的次数
  • 我想自己直接存储身份验证,而不是Spring Security为我做这件事
  • 我手动存储身份验证,我想删除它
  • 我正在使用SessionManagementFilter,我需要有关如何摆脱它的指导
  • 我希望将身份验证存储在会话之外的其他地方
  • 我正在使用无状态身份验证,但我仍然希望将其存储在会话中
  • 我正在使用SessionCreationPolicy.NEVER,但应用程序仍在创建会话。

Understanding Session Management’s Components

(了解会话管理的组件)

会话管理支持由几个组件组成,这些组件协同工作以提供功能。这些组件是SecurityContextHolderFilter、SecurityContextPersistenceFilter和SessionManagementFilter。

注意:
在Spring Security 6中,SecurityContextPersistenceFilter和SessionManagementFilter是默认不设置的。除此之外,任何应用程序都应该只设置SecurityContextHolderFilter或SecurityContextPersistenceFilter中的一个,而不是同时设置。

The SessionManagementFilter

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

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

Moving Away From SessionManagementFilter

(远离SessionManagementFilter)

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

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

Things To Consider When Moving Away From SessionManagementFilter

(远离SessionManagementFilter时要考虑的事情)

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

Method 替换
sessionAuthenticationErrorUrl 在身份验证机制中配置AuthenticationFailureHandler
sessionAuthenticationFailureHandler 在身份验证机制中配置AuthenticationFailureHandler
sessionAuthenticationStrategy 如上所述,在身份验证机制中配置SessionAuthenticationStrategy

如果您尝试使用这些方法中的任何一个,都会引发异常。

Customizing Where the Authentication Is Stored

(自定义认证存储位置)

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

  • 您可能需要在HttpSessionSecurityContextRepository实例上调用单独的setter
  • 您可能希望将安全上下文存储在缓存或数据库中,以启用水平扩展

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

自定义SecurityContextRepository

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

注意:
上面的配置将在SecurityContextHolderFilter和参与的身份验证filters(如UsernamePasswordAuthenticationFilter)上设置SecurityContextRepository。要在无状态筛选器中设置它,请参阅如何为无状态身份验证自定义SecurityContextRepository。

Storing the Authentication manually

(手动保存身份验证)

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

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并在其中设置Authentication
  6. 将SecurityContext保存在SecurityContextRepository中

就是这样。如果你不确定上面例子中的securityContextHolderStrategy是什么,你可以在使用SecurityContextStrategy一节中阅读更多关于它的信息。

Properly Clearing an Authentication

(正确清除身份验证)

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

Configuring Persistence for Stateless Authentication

(配置无状态认证的持久性)

有时不需要创建和维护HttpSession,例如,在请求之间持久化身份验证。有些身份验证机制(如HTTP Basic)是无状态的,因此在每次请求时都要重新对用户进行身份验证。

如果您不希望创建会话,您可以使用SessionCreationPolicy.STATELESS,像这样:

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

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

如果您正在使用SessionCreationPolicy.NEVER,您可能会注意到应用程序仍在创建HttpSession。在大多数情况下,发生这种情况是因为请求保存在会话中,以便在身份验证成功后重新请求经过身份验证的资源。要避免这种情况,请参考如何防止被保存请求一节。

Storing Stateless Authentication in the Session

(在会话中存储无状态身份验证)

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

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

在HttpSession中存储HTTP Base 身份验证

@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();
}

上述内容也适用于其他认证机制,如Bearer Token Authentication。

Understanding Require Explicit Save

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

出于这些原因,SecurityContextPersistenceFilter已被弃用,取而代之的是SecurityContextHolderFilter。在Spring Security 6中,默认行为是SecurityContextHolderFilter将仅从SecurityContextRepository读取SecurityContext并将其填充到SecurityContextHolder中。如果用户希望SecurityContext在请求之间保持不变,那么他们现在必须显式地将SecurityContext与SecurityContextRepository一起保存。这消除了歧义,并通过只在必要时要求写入SecurityContextRepository(即HttpSession)来提高性能。

How it works

(它是如何工作的)
总而言之,当requireExplicitSave为true时,Spring Security会设置SecurityContextHolderFilter而不是SecurityContextPersistenceFilter。

Configuring Concurrent Session Control

(配置并发会话控制)

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

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

然后将以下行添加到安全配置中:

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

这将防止用户多次登录—第二次登录将导致第一次登录无效。
使用Spring Boot,您可以通过以下方式测试上述配置场景:

@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());
    }

}

您可以使用Maximum Sessions示例进行尝试。

同样常见的情况是,您希望阻止第二次登录,在这种情况下,您可以使用:

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

第二次登录将被拒绝。通过“拒绝”,我们的意思是如果使用基于表单的登录,用户将被发送到authentication-failure-url。如果通过另一种非交互机制(如“"remember-me”)进行第二次身份验证,则将向客户端发送“unauthorized”(401)错误。如果希望使用错误页面,则可以向会话管理元素添加属性session-authentication-error-url。

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

@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());
    }

}

如果您使用自定义的身份验证过滤器进行基于表单的登录,则必须显式配置并发会话控制支持。您可以使用“最大会话阻止登录”示例进行尝试。

Detecting Timeouts

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

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

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

Customizing the Invalid Session Strategy

(自定义无效会话策略)

invalidSessionUrl是使用SimpleRedirectInvalidSessionStrategy实现设置InvalidSessionStrategy的方便方法。如果你想自定义行为,你可以实现InvalidSessionStrategy接口,并使用InvalidSessionStrategy方法配置它:

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

Clearing Session Cookies on Logout

(注销时清除会话Cookie)

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

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

这样做的优点是与容器无关,并且可以与任何支持Clear-Site-Data头的容器一起工作。

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

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

不幸的是,不能保证这对每个servlet容器都有效,因此需要在您的环境中进行测试。

注意:
如果您在代理服务器后面运行应用程序,您还可以通过配置代理服务器来删除会话cookie。例如,通过使用Apache HTTPD的mod_headers,以下指令通过在响应注销请求时使JSESSIONID cookie过期来删除该cookie(假设应用程序部署在/tutorial路径下):


Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
LocationMatch>

有关“清除站点数据”和“注销”部分的更多详细信息。

Understanding Session Fixation Attack Protection

(了解会话固定攻击保护)

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

Configuring Session Fixation Protection

(配置会话固定保护)

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

  • changeSessionId -不创建新会话。相反,使用Servlet容器提供的会话固定保护(HttpServletRequest#changeSessionId())。此选项仅在Servlet 3.1 (Java EE 7)和更新的容器中可用。在旧的容器中指定它将导致异常。这是Servlet 3.1和更新的容器中的默认值。
  • newSession -创建一个新的“干净”会话,不复制现有会话数据(Spring security相关属性仍将被复制)。
  • migrateSession -创建一个新会话,并将所有现有会话属性复制到新会话。这是Servlet 3.0或更早的容器中的默认值。

您可以通过执行以下操作来配置会话固定保护:

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

发生会话固定保护时,会导致在应用程序上下文中发布SessionFixationProtectionEvent。如果您使用changeSessionId,这种保护也会导致任何jakarta.servlet.http.HttpSessionIdListeners收到通知,因此,如果您的代码同时侦听这两个事件,请小心。

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

Using SecurityContextHolderStrategy

考虑以下代码块:

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的机会。为此,您应该将代码更改为以下内容:

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来实现,它可以这样配置:

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

What to read next

(接下来要读什么)

  • 与Spring Session的集群会话
  1. SessionManagementFilter不会检测到在身份验证之后执行重定向的机制进行的身份验证(例如表单登录),因为在身份验证请求期间不会调用过滤器。在这些情况下,会话管理功能必须单独处理。

你可能感兴趣的:(Spring,Security,spring-security,spring)