Authentication Persistence and Session Management

在你获取到 authenticating requests 的应用程序时,考虑将来请求中如何保留和恢复该授权结果非常重要。

Once you have got an application that is authenticating requests, it is important to consider how that resulting authentication will be persisted and restored on future requests.

默认情况下,此操作会自动完成,因此无需编写额外的代码,不过了解 HttpSecurityrequireExplicitSave 的含义非常重要。

This is done automatically by default, so no additional code is necessary, though it is important to know what requireExplicitSave means in HttpSecurity.

如果愿意,可以使用 you can read more about what requireExplicitSave is doingwhy it’s important。否则,大多数情况下您都已完成本部分内容。

If you like, how-it-works-requireexplicitsave or requireexplicitsave. Otherwise, in most cases you are done with this section.

但在结束之前,请考虑以下这些用例是否适合您的应用程序:

But before you leave, consider if any of these use cases fit your application:

Understanding Session Management’s Components

会话管理支持由几个组件组成,这些组件协同工作以提供该功能。这些组件是 the SecurityContextHolderFilterthe SecurityContextPersistenceFilterthe SessionManagementFilter

The Session Management support is composed of a few components that work together to provide the functionality. Those components are, the SecurityContextHolderFilter, the SecurityContextPersistenceFilter and the-sessionmanagementfilter.

在 Spring Security 6 中,默认情况下不会设置 SecurityContextPersistenceFilterSessionManagementFilter。此外,任何应用程序都应该只设置 SecurityContextHolderFilterSecurityContextPersistenceFilter,而不能同时设置这两个参数。

In Spring Security 6, the SecurityContextPersistenceFilter and SessionManagementFilter are not set by default. In addition to that, any application should only have either SecurityContextHolderFilter or SecurityContextPersistenceFilter set, never both.

The SessionManagementFilter

SessionManagementFilter 会将 SecurityContextRepository 的内容与 SecurityContextHolder 的当前内容进行比较,以确定用户是否在当前请求期间经过身份验证,通常是通过非交互式身份验证机制,例如预身份验证或记住我。这种身份验证机制在身份验证后执行重定向(例如表单登录),不会被 SessionManagementFilter 检测到,因为该过滤器不会在身份验证请求期间调用。在这些情况下,需要单独处理会话管理功能。如果该存储库包含安全上下文,则过滤器不会执行任何操作。如果它不包含,并且线程局部 SecurityContext 包含一个(非匿名)Authentication 对象,则该过滤器会认为它们已通过堆栈中的先前过滤器进行身份验证。然后,它将调用已配置的 SessionAuthenticationStrategy

The SessionManagementFilter checks the contents of the SecurityContextRepository against the current contents of the SecurityContextHolder to determine whether a user has been authenticated during the current request, typically by a non-interactive authentication mechanism, such as pre-authentication or remember-me Authentication by mechanisms which perform a redirect after authenticating (such as form-login) will not be detected by SessionManagementFilter, as the filter will not be invoked during the authenticating request. Session-management functionality has to be handled separately in these cases.. If the repository contains a security context, the filter does nothing. If it doesn’t, and the thread-local SecurityContext contains a (non-anonymous) Authentication object, the filter assumes they have been authenticated by a previous filter in the stack. It will then invoke the configured SessionAuthenticationStrategy.

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

If the user is not currently authenticated, the filter will check whether an invalid session ID has been requested (because of a timeout, for example) and will invoke the configured InvalidSessionStrategy, if one is set. The most common behaviour is just to redirect to a fixed URL and this is encapsulated in the standard implementation SimpleRedirectInvalidSessionStrategy. The latter is also used when configuring an invalid session URL through the namespace, session-mgmt.

Moving Away From SessionManagementFilter

在 Spring Security 5 中,默认配置依赖 SessionManagementFilter 来检测用户是否刚刚完成授权,并调用 {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[SessionAuthenticationStrategy]。这样做的一个问题是,这表示在典型设置中,HttpSession 必须对每个请求进行读取。

In Spring Security 5, the default configuration relies on SessionManagementFilter to detect if a user just authenticated and invoke {security-api-url}org/springframework/security/web/authentication/session/SessionAuthenticationStrategy.html[the SessionAuthenticationStrategy]. The problem with this is that it means that in a typical setup, the HttpSession must be read for every request.

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

In Spring Security 6, the default is that authentication mechanisms themselves must invoke the SessionAuthenticationStrategy. This means that there is no need to detect when Authentication is done and thus the HttpSession does not need to be read for every request.

Things To Consider When Moving Away From SessionManagementFilter

在 Spring Security 6 中,默认情况下不使用 SessionManagementFilter,因此来自 sessionManagement DSL 的一些方法将不起作用。

In Spring Security 6, the SessionManagementFilter is not used by default, therefore, some methods from the sessionManagement DSL will not have any effect.

Method Replacement

sessionAuthenticationErrorUrl

Configure an {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[AuthenticationFailureHandler] in your authentication mechanism

sessionAuthenticationFailureHandler

Configure an {security-api-url}/org/springframework/security/web/authentication/AuthenticationFailureHandler.html[AuthenticationFailureHandler] in your authentication mechanism

sessionAuthenticationStrategy

Configure an SessionAuthenticationStrategy in your authentication mechanism as moving-away-from-sessionmanagementfilter

如果您尝试使用这些方法中的任何一个,将抛出异常。

If you try to use any of these methods, an exception will be thrown.

Customizing Where the Authentication Is Stored

默认情况下,Spring Security 为您将安全上下文存储在 HTTP 会话中。但是,这里有几个您可能需要自定义它的原因:

By default, Spring Security stores the security context for you in the HTTP session. However, here are several reasons you may want to customize that:

  • You may want to call individual setters on the HttpSessionSecurityContextRepository instance

  • You may want to store the security context in a cache or database to enable horizontal scaling

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

First, you need to create an implementation of SecurityContextRepository or use an existing implementation like HttpSessionSecurityContextRepository, then you can set it in HttpSecurity.

Customizing the SecurityContextRepository
  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    SecurityContextRepository repo = new MyCustomSecurityContextRepository();
    http
        // ...
        .securityContext((context) -> context
            .securityContextRepository(repo)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    val repo = MyCustomSecurityContextRepository()
    http {
        // ...
        securityContext {
            securityContextRepository = repo
        }
    }
    return http.build()
}
<http security-context-repository-ref="repo">
    <!-- ... -->
</http>
<bean name="repo" class="com.example.MyCustomSecurityContextRepository" />

以上配置在 SecurityContextHolderFilterparticipating 身份验证过滤器上设置了 SecurityContextRepository,与 UsernamePasswordAuthenticationFilter 一样。要也在无状态过滤器中设置它,请参阅 how to customize the SecurityContextRepository for Stateless Authentication

The above configuration sets the SecurityContextRepository on the SecurityContextHolderFilter and participating authentication filters, like UsernamePasswordAuthenticationFilter. To also set it in stateless filters, please see storing-stateless-authentication-in-the-session.

如果您使用的是自定义身份验证机制,您可能希望 store the Authentication by yourself

If you are using a custom authentication mechanism, you might want to store-authentication-manually.

Storing the Authentication manually

在某些情况下,例如,你可能手动授权用户,而不是依赖 Spring Security 过滤器。你可以使用一个自定义过滤器或 {spring-framework-reference-url}/web.html#mvc-controller[Spring MVC 控制器] 端点来执行此操作。如果你想在请求之间保存授权(例如,在 HttpSession 中),你必须执行以下操作:

In some cases, for example, you might be authenticating a user manually instead of relying on Spring Security filters. You can use a custom filters or a {spring-framework-reference-url}/web.html#mvc-controller[Spring MVC controller] endpoint to do that. If you want to save the authentication between requests, in the HttpSession, for example, you have to do so:

  • 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 Add the SecurityContextRepository to the controller
2 Inject the HttpServletRequest and HttpServletResponse to be able to save the SecurityContext
3 Create an unauthenticated UsernamePasswordAuthenticationToken using the provided credentials
4 Call AuthenticationManager#authenticate to authenticate the user
5 Create a SecurityContext and set the Authentication in it
6 Save the SecurityContext in the SecurityContextRepository

就是这样。如果您不确定上述示例中的 securityContextHolderStrategy 是什么,您可以在 Using SecurityContextStrategy section 中阅读更多相关内容。

And that’s it. If you are not sure what securityContextHolderStrategy is in the above example, you can read more about it in the use-securitycontextholderstrategy.

Properly Clearing an Authentication

如果你正在使用 Spring Security 的 Logout Support,那么它可以为你处理很多事情,包括清除和保存上下文。但是,假设你需要手动让用户注销你的应用,在这种情况下,你需要确保你 clearing and saving the context properly

If you are using Spring Security’s Logout Support then it handles a lot of stuff for you including clearing and saving the context. But, let’s say you need to manually log users out of your app. In that case, you’ll need to make sure you’re clearing and saving the context properly.

Configuring Persistence for Stateless Authentication

有时,不需要创建和维护 HttpSession,例如为了对请求进行授权保留。一些授权机制(例如 HTTP Basic)是无状态的,因此,会在每个请求中对用户进行重新授权。

Sometimes there is no need to create and maintain a HttpSession for example, to persist the authentication across requests. Some authentication mechanisms like HTTP Basic are stateless and, therefore, re-authenticates the user on every request.

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

If you do not wish to create sessions, you can use SessionCreationPolicy.STATELESS, like so:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        // ...
        .sessionManagement((session) -> session
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        // ...
        sessionManagement {
            sessionCreationPolicy = SessionCreationPolicy.STATELESS
        }
    }
    return http.build()
}
<http create-session="stateless">
    <!-- ... -->
</http>

上述配置 configuring the SecurityContextRepository 使用 NullSecurityContextRepository,并且还 preventing the request from being saved in the session

The above configuration is customizing-where-authentication-is-stored to use a NullSecurityContextRepository and is also preventing the request from being saved in the session.

如果你正在使用 SessionCreationPolicy.NEVER,你可能会注意到应用程序仍在创建一个 HttpSession。在大多数情况下,这样做是因为 request is saved in the session 需要在身份验证成功后重新请求已验证的资源。为了避免这种情况,请参阅 how to prevent the request of being saved 部分。

If you are using SessionCreationPolicy.NEVER, you might notice that the application is still creating a HttpSession. In most cases, this happens because the request is saved in the session for the authenticated resource to re-request after authentication is successful. To avoid that, please refer to how to prevent the request of being saved section.

Storing Stateless Authentication in the Session

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

If, for some reason, you are using a stateless authentication mechanism, but you still want to store the authentication in the session you can use the HttpSessionSecurityContextRepository instead of the NullSecurityContextRepository.

对于 HTTP Basic,你可以添加 a ObjectPostProcessor,它会更改 BasicAuthenticationFilter 所使用的 SecurityContextRepository

For the HTTP Basic, you can add a ObjectPostProcessor that changes the SecurityContextRepository used by the BasicAuthenticationFilter:

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

上述内容同样适用于其他授权机制(例如 Bearer Token Authentication)。

The above also applies to others authentication mechanisms, like Bearer Token Authentication.

Understanding Require Explicit Save

在 Spring Security 5 中,默认行为是通过 <<`SecurityContextPersistenceFilter`,securitycontextpersistencefilter>> 自动将 SecurityContext 保存到 SecurityContextRepository。必须在提交 HttpServletResponse 之前且在 SecurityContextPersistenceFilter 之前执行保存。但是,当在请求完成之前(即在提交 HttpServletResponse 之前)自动保存 SecurityContext 时,可能会让用户感到意外。另外,跟踪该状态以确定是否需要保存也很复杂,这可能会在 SecurityContextRepository(即 HttpSession)进行不必要的写入。

In Spring Security 5, the default behavior is for the SecurityContext to automatically be saved to the SecurityContextRepository using the <<`SecurityContextPersistenceFilter`,securitycontextpersistencefilter>>. Saving must be done just prior to the HttpServletResponse being committed and just before SecurityContextPersistenceFilter. Unfortunately, automatic persistence of the SecurityContext can surprise users when it is done prior to the request completing (i.e. just prior to committing the HttpServletResponse). It also is complex to keep track of the state to determine if a save is necessary causing unnecessary writes to the SecurityContextRepository (i.e. HttpSession) at times.

由于这些原因,SecurityContextPersistenceFilter 已被弃用,取而代之的是 SecurityContextHolderFilter。在 Spring Security 6 中,默认行为是 the SecurityContextHolderFilter 将只从 SecurityContextRepository 中读取 SecurityContext 并将其填充到 SecurityContextHolder 中。现在,如果用户希望 SecurityContext 在请求之间持续存在,他们必须使用 SecurityContextRepository 显式保存 SecurityContext。这样消除了歧义,并且只在需要时写入 SecurityContextRepository(即 HttpSession),从而提高了性能。

For these reasons, the SecurityContextPersistenceFilter has been deprecated to be replaced with the SecurityContextHolderFilter. In Spring Security 6, the default behavior is that the SecurityContextHolderFilter will only read the SecurityContext from SecurityContextRepository and populate it in the SecurityContextHolder. Users now must explicitly save the SecurityContext with the SecurityContextRepository if they want the SecurityContext to persist between requests. This removes ambiguity and improves performance by only requiring writing to the SecurityContextRepository (i.e. HttpSession) when it is necessary.

How it works

简而言之,当 requireExplicitSavetrue 时,Spring Security 设置 the SecurityContextHolderFilter 而不是 the SecurityContextPersistenceFilter

In summary, when requireExplicitSave is true, Spring Security sets up the SecurityContextHolderFilter instead of the SecurityContextPersistenceFilter

Configuring Concurrent Session Control

如果你希望限制某个用户登录到你的应用程序的能力,Spring Security 通过以下简单的附加项来开箱即用的支持此功能。首先,你需要向你的配置中添加以下侦听器,以使 Spring Security 了解会话生命周期事件:

If you wish to place constraints on a single user’s ability to log in to your application, Spring Security supports this out of the box with the following simple additions. First, you need to add the following listener to your configuration to keep Spring Security updated about session lifecycle events:

  • Java

  • Kotlin

  • web.xml

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
@Bean
open fun httpSessionEventPublisher(): HttpSessionEventPublisher {
    return HttpSessionEventPublisher()
}
<listener>
<listener-class>
    org.springframework.security.web.session.HttpSessionEventPublisher
</listener-class>
</listener>

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

Then add the following lines to your security configuration:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            sessionConcurrency {
                maximumSessions = 1
            }
        }
    }
    return http.build()
}
<http>
...
<session-management>
    <concurrency-control max-sessions="1" />
</session-management>
</http>

这将防止用户多次登录——第二次登录会导致第一次登录无效。

This will prevent a user from logging in multiple times - a second login will cause the first to be invalidated.

使用 Spring Boot,你可以按以下方式测试上述配置方案:

Using Spring Boot, you can test the above configuration scenario the following way:

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

}

你可以使用 最多会话示例 来尝试此操作。

You can try it using the Maximum Sessions sample.

通常,你可能更愿意防止第二次登录,在这种情况下你可以使用:

It is also common that you would prefer to prevent a second login, in which case you can use:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            sessionConcurrency {
                maximumSessions = 1
                maxSessionsPreventsLogin = true
            }
        }
    }
    return http.build()
}
<http>
<session-management>
    <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>

第二次登录将被拒绝。“拒绝”是指如果使用基于表单的登录,系统会将用户发送到 authentication-failure-url。如果第二次身份验证通过另一个非交互机制(例如“记住我”)进行,系统会向客户端发送“未授权”(401)错误。如果你希望使用错误页面,则可以将属性 session-authentication-error-url 添加到 session-management 元素中。

The second login will then be rejected. By "rejected", we mean that the user will be sent to the authentication-failure-url if form-based login is being used. If the second authentication takes place through another non-interactive mechanism, such as "remember-me", an "unauthorized" (401) error will be sent to the client. If instead you want to use an error page, you can add the attribute session-authentication-error-url to the session-management element.

使用 Spring Boot,你可以按以下方式测试上述配置:

Using Spring Boot, you can test the above configuration the following way:

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

}

如果你正在为基于表单的登录使用一个定制的授权过滤器,那么你必须显式配置并发会话控制支持。你可以使用 最多会话阻止登录示例 来尝试此操作。

If you are using a customized authentication filter for form-based login, then you have to configure concurrent session control support explicitly. You can try it using the Maximum Sessions Prevent Login sample.

Detecting Timeouts

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

Sessions expire on their own, and there is nothing that needs to be done to ensure that a security context gets removed. That said, Spring Security can detect when a session has expired and take specific actions that you indicate. For example, you may want to redirect to a specific endpoint when a user makes a request with an already-expired session. This is achieved through the invalidSessionUrl in HttpSecurity:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/invalidSession")
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            invalidSessionUrl = "/invalidSession"
        }
    }
    return http.build()
}
<http>
...
<session-management invalid-session-url="/invalidSession" />
</http>

请注意,如果你使用此机制来检测会话超时,如果用户注销然后再登录而不关闭浏览器,则可能会错误地报告错误。这是因为会话 cookie 在你使会话无效时不会被清除,即使用户已注销,也会被重新提交。如果是这种情况,你可能希望 configure logout to clear the session cookie

Note that if you use this mechanism to detect session timeouts, it may falsely report an error if the user logs out and then logs back in without closing the browser. This is because the session cookie is not cleared when you invalidate the session and will be resubmitted even if the user has logged out. If that is your case, you might want to clearing-session-cookie-on-logout.

Customizing the Invalid Session Strategy

invalidSessionUrl 是一个便捷方法,用于使用 {security-api-url}/org/springframework/security/web/session/SimpleRedirectInvalidSessionStrategy.html[ SimpleRedirectInvalidSessionStrategy 实现] 设置 InvalidSessionStrategy。如果你想要定制此行为,那么你可以实现 {security-api-url}/org/springframework/security/web/session/InvalidSessionStrategy.html[InvalidSessionStrategy] 接口,并使用 invalidSessionStrategy 方法对其进行配置:

The invalidSessionUrl is a convenience method for setting the InvalidSessionStrategy using the {security-api-url}/org/springframework/security/web/session/SimpleRedirectInvalidSessionStrategy.html[SimpleRedirectInvalidSessionStrategy implementation]. If you want to customize the behavior, you can implement the {security-api-url}/org/springframework/security/web/session/InvalidSessionStrategy.html[InvalidSessionStrategy] interface and configure it using the invalidSessionStrategy method:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .invalidSessionStrategy(new MyCustomInvalidSessionStrategy())
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            invalidSessionStrategy = MyCustomInvalidSessionStrategy()
        }
    }
    return http.build()
}
<http>
...
<session-management invalid-session-strategy-ref="myCustomInvalidSessionStrategy" />
<bean name="myCustomInvalidSessionStrategy" class="com.example.MyCustomInvalidSessionStrategy" />
</http>

你可以明确删除 JSESSIONID cookie,例如使用注销处理程序中的 link:https://w3c.github.io/webappsec-clear-site-data/[Clear-Site-Data 头:

You can explicitly delete the JSESSIONID cookie on logging out, for example by using the Clear-Site-Data header in the logout handler:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout((logout) -> logout
            .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES)))
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        logout {
            addLogoutHandler(HeaderWriterLogoutHandler(ClearSiteDataHeaderWriter(COOKIES)))
        }
    }
    return http.build()
}
<http>
<logout success-handler-ref="clearSiteDataHandler" />
<b:bean id="clearSiteDataHandler" class="org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler">
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter">
            <b:constructor-arg>
                <b:list>
                    <b:value>COOKIES</b:value>
                </b:list>
            </b:constructor-arg>
        </b:bean>
    </b:constructor-arg>
</b:bean>
</http>

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

This has the advantage of being container agnostic and will work with any container that supports the Clear-Site-Data header.

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

As an alternative, you can also use the following syntax in the logout handler:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID")
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        logout {
            deleteCookies("JSESSIONID")
        }
    }
    return http.build()
}
<http>
  <logout delete-cookies="JSESSIONID" />
</http>

遗憾的是,无法保证这适用于每个 Servlet 容器,因此你需要在你的环境中进行测试。

Unfortunately, this cannot be guaranteed to work with every servlet container, so you need to test it in your environment.

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

If you run your application behind a proxy, you may also be able to remove the session cookie by configuring the proxy server. For example, by using Apache HTTPD’s mod_headers, the following directive deletes the JSESSIONID cookie by expiring it in the response to a logout request (assuming the application is deployed under the /tutorial path):

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

有关 Clear Site DataLogout sections 的更多详细信息。

More details on the Clear Site Data and Logout sections.

Understanding Session Fixation Attack Protection

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

Session fixation attacks are a potential risk where it is possible for a malicious attacker to create a session by accessing a site, then persuade another user to log in with the same session (by sending them a link containing the session identifier as a parameter, for example). Spring Security protects against this automatically by creating a new session or otherwise changing the session ID when a user logs in.

Configuring Session Fixation Protection

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

You can control the strategy for Session Fixation Protection by choosing between three recommended options:

  • changeSessionId - Do not create a new session. Instead, use the session fixation protection provided by the Servlet container (HttpServletRequest#changeSessionId()). This option is only available in Servlet 3.1 (Java EE 7) and newer containers. Specifying it in older containers will result in an exception. This is the default in Servlet 3.1 and newer containers.

  • newSession - Create a new "clean" session, without copying the existing session data (Spring Security-related attributes will still be copied).

  • migrateSession - Create a new session and copy all existing session attributes to the new session. This is the default in Servlet 3.0 or older containers.

你可以通过下列方法配置会话修复防护:

You can configure the session fixation protection by doing:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement((session) -> session
            .sessionFixation((sessionFixation) -> sessionFixation
                .newSession()
            )
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            sessionFixation {
                newSession()
            }
        }
    }
    return http.build()
}
<http>
  <session-management session-fixation-protection="newSession" />
</http>

会话修复保护发生时,会发布 SessionFixationProtectionEvent 并将其发布到应用程序上下文中。如果你使用 changeSessionId,此保护将 also 使任何 jakarta.servlet.http.HttpSessionIdListener 都收到通知,因此,如果你的代码同时侦听两个事件,那么请谨慎使用。

When session fixation protection occurs, it results in a SessionFixationProtectionEvent being published in the application context. If you use changeSessionId, this protection will also result in any `jakarta.servlet.http.HttpSessionIdListener`s being notified, so use caution if your code listens for both events.

您也可以设置会话固定保护为 1 以禁用它,但这是不推荐的,因为它会使您的应用程序容易受到攻击。

You can also set the session fixation protection to none to disable it, but this is not recommended as it leaves your application vulnerable.

Using SecurityContextHolderStrategy

考虑以下代码块:

Consider the following block of code:

  • 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. Creates an empty SecurityContext instance by accessing the SecurityContextHolder statically.

  2. Sets the Authentication object in the SecurityContext instance.

  3. Sets the SecurityContext instance in the SecurityContextHolder statically.

虽然上面的代码运行良好,但它会产生一些不希望的影响:当组件通过 statically 通过访问 SecurityContext SecurityContextHolder 时,当存在多个想要指定 SecurityContextHolderStrategy 的应用程序上下文时,这会创建竞争条件。这是因为在 SecurityContextHolder 中,每个类加载器只有一个策略,而不是每个应用程序上下文一个策略。

While the above code works fine, it can produce some undesired effects: when components access the SecurityContext statically through SecurityContextHolder, this can create race conditions when there are multiple application contexts that want to specify the SecurityContextHolderStrategy. This is because in SecurityContextHolder there is one strategy per classloader instead of one per application context.

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

To address this, components can wire SecurityContextHolderStrategy from the application context. By default, they will still look up the strategy from SecurityContextHolder.

这些更改在很大程度上是内部的,但它们为应用程序提供了自动连接 SecurityContextHolderStrategy 的机会,而不是访问 SecurityContext statically。为此,您应该将代码更改为以下内容:

These changes are largely internal, but they present the opportunity for applications to autowire the SecurityContextHolderStrategy instead of accessing the SecurityContext statically. To do so, you should change the code to the following:

  • 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. Creates an empty SecurityContext instance using the configured SecurityContextHolderStrategy.

  2. Sets the Authentication object in the SecurityContext instance.

  3. Sets the SecurityContext instance in the SecurityContextHolderStrategy.

Forcing Eager Session Creation

有时,迫切创建会话会很有价值。这可以通过使用 {security-api-url}org/springframework/security/web/session/ForceEagerSessionCreationFilter.html[ForceEagerSessionCreationFilter] 来完成,该过滤器可以使用以下配置:

At times, it can be valuable to eagerly create sessions. This can be done by using the {security-api-url}org/springframework/security/web/session/ForceEagerSessionCreationFilter.html[ForceEagerSessionCreationFilter] which can be configured using:

  • Java

  • Kotlin

  • XML

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
    http
        .sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        sessionManagement {
            sessionCreationPolicy = SessionCreationPolicy.ALWAYS
        }
    }
    return http.build()
}
<http create-session="ALWAYS">

</http>

What to read next