对于最新的稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

身份验证持久化与会话管理

一旦你拥有一个能够对请求进行身份验证的应用程序,接下来就需要考虑如何在后续请求中持久化并恢复该身份验证结果。spring-doc.cadn.net.cn

默认情况下,此操作会自动完成,因此无需额外的代码,但了解 requireExplicitSaveHttpSecurity 的含义非常重要。spring-doc.cadn.net.cn

如果你愿意,你可以进一步了解 requireExplicitSave 的作用,或者了解它为何重要。否则,在大多数情况下,你已经完成了本节内容。spring-doc.cadn.net.cn

但在您离开之前,请考虑以下用例是否适用于您的应用程序:spring-doc.cadn.net.cn

了解会话管理的组件

会话管理支持由几个协同工作的组件组成,以提供所需的功能。 这些组件包括 SecurityContextHolderFilterSecurityContextPersistenceFilterSessionManagementFilterspring-doc.cadn.net.cn

在 Spring Security 6 中,SecurityContextPersistenceFilterSessionManagementFilter 默认不会被设置。 此外,任何应用程序都应仅设置 SecurityContextHolderFilterSecurityContextPersistenceFilter 中的一个,绝不应同时设置两者。spring-doc.cadn.net.cn

SessionManagementFilter

SessionManagementFilter 检查 SecurityContextRepository 的内容与当前 SecurityContextHolder 的内容,以判断用户是否在当前请求期间已通过身份验证,通常是通过非交互式身份验证机制(如预身份验证或记住我)完成的[1]。 如果存储库包含安全上下文,则过滤器不执行任何操作。 如果不包含,且线程本地变量 SecurityContext 中包含一个(非匿名)Authentication 对象,则过滤器假设它们已由堆栈中的上一个过滤器完成身份验证。 随后,它将调用配置的 SessionAuthenticationStrategyspring-doc.cadn.net.cn

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

正在远离SessionManagementFilter

在 Spring Security 5 中,默认配置依赖于 SessionManagementFilter 来检测用户是否刚刚完成身份验证,并调用 SessionAuthenticationStrategy。 这样做的问题是,在典型配置中,每个请求都必须读取 HttpSessionspring-doc.cadn.net.cn

在 Spring Security 6 中,默认情况下,认证机制本身必须调用 SessionAuthenticationStrategy。 这意味着无需再检测何时完成 Authentication,因此也就不需要在每个请求中读取 HttpSessionspring-doc.cadn.net.cn

迁移时需要考虑的事项SessionManagementFilter

在 Spring Security 6 中,默认情况下不会使用 SessionManagementFilter,因此 sessionManagement DSL 中的某些方法将不会产生任何效果。spring-doc.cadn.net.cn

方法 替换

sessionAuthenticationErrorUrlspring-doc.cadn.net.cn

在您的认证机制中配置 AuthenticationFailureHandlerspring-doc.cadn.net.cn

sessionAuthenticationFailureHandlerspring-doc.cadn.net.cn

在您的认证机制中配置 AuthenticationFailureHandlerspring-doc.cadn.net.cn

sessionAuthenticationStrategyspring-doc.cadn.net.cn

在您的身份验证机制中配置一个SessionAuthenticationStrategy,如上文所述spring-doc.cadn.net.cn

如果你尝试使用这些方法中的任何一个,将会抛出一个异常。spring-doc.cadn.net.cn

自定义认证信息的存储位置

默认情况下,Spring Security 会将安全上下文存储在 HTTP 会话中。然而,你可能出于以下几个原因希望对其进行自定义:spring-doc.cadn.net.cn

  • 你可能想要在 HttpSessionSecurityContextRepository 实例上调用各个 setter 方法spring-doc.cadn.net.cn

  • 你可能希望将安全上下文存储在缓存或数据库中,以实现水平扩展。spring-doc.cadn.net.cn

首先,您需要创建一个 SecurityContextRepository 的实现,或者使用现有的实现(例如 HttpSessionSecurityContextRepository),然后可以在 HttpSecurity 中进行设置。spring-doc.cadn.net.cn

自定义 SecurityContextRepository
@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" />

上述配置在 SecurityContextRepository参与 的认证过滤器(如 UsernamePasswordAuthenticationFilter)上设置了 SecurityContextHolderFilter。 若要在无状态过滤器中也进行设置,请参阅 如何为无状态认证自定义 SecurityContextRepositoryspring-doc.cadn.net.cn

如果您正在使用自定义认证机制,您可能希望自行存储Authenticationspring-doc.cadn.net.cn

存储Authentication手动

在某些情况下,例如,你可能需要手动对用户进行身份验证,而不是依赖 Spring Security 的过滤器。 你可以使用自定义过滤器或Spring MVC 控制器端点来实现这一点。 如果你希望在请求之间(例如在HttpSession中)保存身份验证信息,则必须显式地进行如下操作:spring-doc.cadn.net.cn

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 注入 HttpServletRequestHttpServletResponse,以便能够保存 SecurityContext
3 使用提供的凭据创建一个未经认证的 UsernamePasswordAuthenticationToken
4 调用 AuthenticationManager#authenticate 来对用户进行身份验证
5 创建一个 SecurityContext 并在其中设置 Authentication
6 SecurityContext 保存在 SecurityContextRepository

到此结束。 如果您不确定上述示例中的 securityContextHolderStrategy 是什么,可以在 使用 SecurityContextStrategy 章节 中阅读更多内容。spring-doc.cadn.net.cn

正确清除认证信息

如果你正在使用 Spring Security 的注销支持,那么它会为你处理大量工作,包括清除和保存上下文。 但是,假设你需要手动将用户从你的应用中注销。在这种情况下,你需要确保自己正确地清除并保存上下文spring-doc.cadn.net.cn

为无状态认证配置持久化

有时无需创建和维护 HttpSession,例如,为了在多个请求之间持久化认证信息。 某些认证机制(如HTTP Basic)是无状态的,因此会在每个请求上重新对用户进行认证。spring-doc.cadn.net.cn

如果你不希望创建会话,可以使用 SessionCreationPolicy.STATELESS,如下所示:spring-doc.cadn.net.cn

@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>

上述配置正在配置SecurityContextRepository以使用NullSecurityContextRepository,并且同时防止请求被保存到会话中spring-doc.cadn.net.cn

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

在会话中存储无状态认证信息

如果由于某些原因,您正在使用无状态的身份验证机制,但仍希望将身份验证信息存储在会话中,则可以使用 HttpSessionSecurityContextRepository,而不是 NullSecurityContextRepositoryspring-doc.cadn.net.cn

对于 HTTP Basic,您可以添加一个 ObjectPostProcessor,以更改 BasicAuthenticationFilter 所使用的 SecurityContextRepositoryspring-doc.cadn.net.cn

将 HTTP 基本身份验证信息存储在 HttpSession
@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 身份验证spring-doc.cadn.net.cn

了解需要显式保存

在 Spring Security 5 中,默认行为是 SecurityContext 使用 SecurityContextPersistenceFilter 自动保存到 SecurityContextRepository。 保存操作必须在 HttpServletResponse 提交之前以及 SecurityContextPersistenceFilter 之前完成。 不幸的是,当 SecurityContext 的自动持久化在请求完成之前(即提交 HttpServletResponse 之前)执行时,可能会让用户感到意外。 此外,跟踪状态以判断是否需要保存也较为复杂,这有时会导致对 SecurityContextRepository 进行不必要的写入(例如 HttpSession)。spring-doc.cadn.net.cn

出于这些原因,SecurityContextPersistenceFilter 已被弃用,并将被 SecurityContextHolderFilter 取代。 在 Spring Security 6 中,默认行为是 SecurityContextHolderFilter 将仅从 SecurityContextRepository 读取 SecurityContext,并将其填充到 SecurityContextHolder 中。 如果用户希望 SecurityContext 在请求之间持久化,现在必须显式地使用 SecurityContextRepository 保存 SecurityContext。 这消除了歧义,并通过仅在必要时才写入 SecurityContextRepository(即 HttpSession)来提升性能。spring-doc.cadn.net.cn

工作原理

总之,当 requireExplicitSavetrue 时,Spring Security 将设置 SecurityContextHolderFilter,而不是 SecurityContextPersistenceFilterspring-doc.cadn.net.cn

配置并发会话控制

如果您希望对单个用户登录您应用程序的能力施加限制,Spring Security 通过以下简单的配置即可原生支持此功能。 首先,您需要在配置中添加以下监听器,以使 Spring Security 能够及时获知会话生命周期事件:spring-doc.cadn.net.cn

@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>

然后在您的安全配置中添加以下几行:spring-doc.cadn.net.cn

@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>

这将防止用户多次登录——第二次登录会导致第一次登录失效。spring-doc.cadn.net.cn

您也可以根据用户身份对此进行调整。 例如,管理员可能可以拥有多个会话:spring-doc.cadn.net.cn

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
	AuthorizationManager<?> isAdmin = AuthorityAuthorizationManager.hasRole("ADMIN");
    http
        .sessionManagement(session -> session
            .maximumSessions((authentication) -> isAdmin.authorize(() -> authentication, null).isGranted() ? -1 : 1)
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    val isAdmin: AuthorizationManager<*> = AuthorityAuthorizationManager.hasRole("ADMIN")
    http {
        sessionManagement {
            sessionConcurrency {
                maximumSessions {
                    authentication -> if (isAdmin.authorize({ authentication }, null)!!.isGranted) -1 else 1
                }
            }
        }
    }
    return http.build()
}
<http>
...
<session-management>
    <concurrency-control max-sessions-ref="sessionLimit" />
</session-management>
</http>

<b:bean id="sessionLimit" class="my.SessionLimitImplementation"/>

使用 Spring Boot,你可以通过以下方式测试上述配置:spring-doc.cadn.net.cn

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

}

您可以使用最大会话数示例进行尝试。spring-doc.cadn.net.cn

通常情况下,您可能还希望防止用户重复登录,此时您可以使用:spring-doc.cadn.net.cn

@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 属性。spring-doc.cadn.net.cn

使用 Spring Boot,你可以通过以下方式测试上述配置:spring-doc.cadn.net.cn

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

}

如果你正在使用自定义的身份验证过滤器进行基于表单的登录,那么你必须显式地配置并发会话控制支持。 你可以通过最大会话数阻止登录示例来尝试它。spring-doc.cadn.net.cn

如果你使用的是自定义的 UserDetails 实现,请确保重写 equals()hashCode() 方法。 Spring Security 中默认的 SessionRegistry 实现依赖于一个基于内存的 Map,该 Map 使用这些方法来正确识别和管理用户会话。 如果不重写这些方法,可能会导致会话跟踪和用户比较行为异常。spring-doc.cadn.net.cn

检测超时

会话会自行过期,无需执行任何操作来确保安全上下文被移除。 也就是说,Spring Security 能够检测到会话何时过期,并执行你指定的特定操作。 例如,当用户使用一个已经过期的会话发起请求时,你可能希望将其重定向到某个特定的端点。 这可以通过 invalidSessionUrl 中的 HttpSecurity 来实现:spring-doc.cadn.net.cn

@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 并不会被清除,即使用户已注销,该 Cookie 仍会被重新提交。 如果是这种情况,您可能需要配置注销操作以清除会话 Cookiespring-doc.cadn.net.cn

自定义无效会话策略

invalidSessionUrl 是一个便捷方法,用于通过 SimpleRedirectInvalidSessionStrategy 实现 设置 InvalidSessionStrategy。 如果您希望自定义行为,可以实现 InvalidSessionStrategy 接口,并使用 invalidSessionStrategy 方法进行配置:spring-doc.cadn.net.cn

@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,例如在注销处理器中使用 Clear-Site-Data 标头spring-doc.cadn.net.cn

@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 响应头的容器。spring-doc.cadn.net.cn

作为替代方案,您也可以在注销处理器中使用以下语法:spring-doc.cadn.net.cn

@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 容器中都能正常工作,因此您需要在自己的环境中进行测试。spring-doc.cadn.net.cn

如果你的应用程序运行在代理服务器之后,也可以通过配置代理服务器来移除会话 cookie。 例如,使用 Apache HTTPD 的 mod_headers 模块,以下指令会在处理登出请求的响应中通过设置过期时间来删除 JSESSIONID cookie(假设应用程序部署在 /tutorial 路径下):spring-doc.cadn.net.cn

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

有关清除站点数据注销部分的更多详细信息。spring-doc.cadn.net.cn

了解会话固定攻击防护

会话固定攻击是一种潜在风险,恶意攻击者可以通过访问网站创建一个会话,然后诱使其他用户使用相同的会话登录(例如,向他们发送一个包含会话标识符作为参数的链接)。 Spring Security 在用户登录时会自动通过创建新会话或更改会话 ID 来防范此类攻击。spring-doc.cadn.net.cn

配置会话固定保护

您可以通过在三种推荐选项中进行选择,来控制会话固定保护(Session Fixation Protection)的策略:spring-doc.cadn.net.cn

  • changeSessionId - 不要创建新会话。 而是使用 Servlet 容器提供的会话固定保护机制(HttpServletRequest#changeSessionId())。 此选项仅在 Servlet 3.1(Java EE 7)及更新版本的容器中可用。 在旧版容器中指定该选项将导致异常。 在 Servlet 3.1 及更新版本的容器中,这是默认行为。spring-doc.cadn.net.cn

  • newSession - 创建一个新的“干净”会话,不复制现有会话数据(与 Spring Security 相关的属性仍将被复制)。spring-doc.cadn.net.cn

  • migrateSession - 创建一个新会话,并将所有现有会话属性复制到新会话中。 这在 Servlet 3.0 或更旧版本的容器中是默认行为。spring-doc.cadn.net.cn

您可以通过以下方式配置会话固定保护:spring-doc.cadn.net.cn

@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,这种保护机制还会通知所有注册的 jakarta.servlet.http.HttpSessionIdListener 监听器,因此如果你的代码同时监听这两种事件,请务必谨慎处理。spring-doc.cadn.net.cn

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

使用SecurityContextHolderStrategy

请考虑以下代码块:spring-doc.cadn.net.cn

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. 通过静态访问 SecurityContext 创建一个空的 SecurityContextHolder 实例。spring-doc.cadn.net.cn

  2. Authentication 实例中设置 SecurityContext 对象。spring-doc.cadn.net.cn

  3. SecurityContext 实例静态地设置到 SecurityContextHolder 中。spring-doc.cadn.net.cn

尽管上述代码可以正常工作,但它可能会产生一些不良影响:当组件通过 SecurityContext 以静态方式访问 SecurityContextHolder 时,如果存在多个应用程序上下文都希望指定自己的 SecurityContextHolderStrategy,就可能引发竞态条件。 这是因为 SecurityContextHolder 中每个类加载器(classloader)只对应一个策略,而不是每个应用程序上下文对应一个策略。spring-doc.cadn.net.cn

为了解决这个问题,组件可以从应用程序上下文中注入 SecurityContextHolderStrategy。 默认情况下,它们仍会从 SecurityContextHolder 中查找该策略。spring-doc.cadn.net.cn

这些更改主要是内部的,但它们为应用程序提供了机会,可以通过自动装配 SecurityContextHolderStrategy 来替代以静态方式访问 SecurityContext。 为此,您应将代码修改为如下所示:spring-doc.cadn.net.cn

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. 使用配置的 SecurityContext 创建一个空的 SecurityContextHolderStrategy 实例。spring-doc.cadn.net.cn

  2. Authentication 实例中设置 SecurityContext 对象。spring-doc.cadn.net.cn

  3. SecurityContext 中设置 SecurityContextHolderStrategy 实例。spring-doc.cadn.net.cn

强制急切创建会话

有时,急切地创建会话可能是有价值的。 这可以通过使用 ForceEagerSessionCreationFilter 来实现,其配置方式如下:spring-doc.cadn.net.cn

@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>

接下来阅读什么


1. 通过在认证后执行重定向的机制(例如表单登录)进行的认证,将不会被 SessionManagementFilter 检测到,因为在认证请求过程中该过滤器不会被调用。在这种情况下,会话管理功能必须单独处理。