跳到主要内容

OIDC 注销

QWen Max 中英对照 OIDC Logout

一旦最终用户能够登录到您的应用程序,就需要考虑他们将如何退出登录。

一般来说,有三种用例需要考虑:

  1. 我只想执行本地登出

  2. 我想由我的应用程序发起,同时登出我的应用程序和 OIDC 提供者

  3. 我想由 OIDC 提供者发起,同时登出我的应用程序和 OIDC 提供者

本地注销

要执行本地注销,无需特殊的OIDC配置。Spring Security 会自动设置一个本地注销端点,你可以通过 logout() DSL 进行配置

OpenID Connect 1.0 客户端发起的注销

OpenID Connect 会话管理 1.0 允许通过 Client 在 Provider 处注销最终用户。可用的策略之一是 RP 发起的注销

如果 OpenID 提供者同时支持会话管理和发现,客户端可以从 OpenID 提供者的发现元数据中获取 end_session_endpoint URL。您可以通过使用 issuer-uri 配置 ClientRegistration 来实现这一点,如下所示:

spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
...
provider:
okta:
issuer-uri: https://dev-1234.oktapreview.com
yaml

此外,你应该配置 OidcClientInitiatedLogoutSuccessHandler,它实现了RP发起的注销,如下所示:

@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

@Autowired
private ClientRegistrationRepository clientRegistrationRepository;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.logout(logout -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
return http.build();
}

private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);

// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");

return oidcLogoutSuccessHandler;
}
}
java
备注

OidcClientInitiatedLogoutSuccessHandler 支持 {baseUrl} 占位符。如果使用,应用程序的基本 URL(如 [app.example.org](https://app.example.org))将在请求时替换它。

OpenID Connect 1.0 后通道登出

OpenID Connect 会话管理 1.0 允许通过让提供商向客户端发起 API 调用来注销最终用户。这被称为 OIDC 后通道注销

要启用此功能,您可以在DSL中设置Back-Channel Logout端点,如下所示:

@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler() {
return new OidcBackChannelLogoutHandler();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.oidcLogout((logout) -> logout
.backChannel(Customizer.withDefaults())
);
return http.build();
}
java

然后,你需要一种方法来监听由 Spring Security 发布的事件,以移除旧的 OidcSessionInformation 条目,如下所示:

@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
return new HttpSessionEventPublisher();
}
java

这将使得如果调用了 HttpSession#invalidate,那么该会话也会从内存中移除。

就这样!

这将启动端点 /logout/connect/back-channel/{registrationId},OIDC 提供者可以通过该端点请求使应用程序中特定最终用户的会话失效。

备注

oidcLogout 需要也配置 oauth2Login

备注

oidcLogout 要求会话 cookie 被命名为 JSESSIONID,以便通过 backchannel 正确注销每个会话。

Back-Channel Logout 架构

考虑一个标识符为 registrationIdClientRegistration

整体的Back-Channel注销流程如下:

  1. 在登录时,Spring Security 会将其 OidcSessionRegistry 实现中的 ID 令牌、CSRF 令牌和提供程序会话 ID(如果有)与应用程序的会话 ID 关联起来。

  2. 然后在注销时,你的 OIDC 提供商会向 /logout/connect/back-channel/registrationId 发起 API 调用,其中包含一个注销令牌,该令牌指示要注销的 sub(最终用户)或 sid(提供程序会话 ID)。

  3. Spring Security 会验证令牌的签名和声明。

  4. 如果令牌包含 sid 声明,则只有与该提供程序会话关联的客户端会话会被终止。

  5. 否则,如果令牌包含 sub 声明,则该最终用户的该客户端的所有会话都将被终止。

备注

请记住,Spring Security 的 OIDC 支持是多租户的。这意味着它只会终止那些 Client 与 Logout Token 中的 aud 声明相匹配的会话。

这个架构的实现中一个值得注意的部分是,它会为每个对应的会话内部传播传入的后通道请求。乍一看,这似乎是不必要的。然而,请记住,Servlet API 并不直接访问 HttpSession 存储。通过进行内部注销调用,现在可以验证相应的会话。

此外,内部伪造一个注销调用允许针对该会话和相应的SecurityContext运行每组LogoutHandler

自定义会话登出端点

发布了 OidcBackChannelLogoutHandler 后,会话注销端点是 {baseUrl}/logout/connect/back-channel/{registrationId}

如果 OidcBackChannelLogoutHandler 没有连接,则 URL 为 {baseUrl}/logout/connect/back-channel/{registrationId},这是不推荐的,因为它需要传递一个 CSRF 令牌,这根据你的应用程序使用的存储库类型可能会很有挑战性。

如果需要自定义端点,可以按如下方式提供 URL:

http
// ...
.oidcLogout((oidc) -> oidc
.backChannel((backChannel) -> backChannel
.logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
)
);

默认情况下,会话注销端点使用 JSESSIONID cookie 将会话关联到相应的 OidcSessionInformation

然而,Spring Session 中默认的cookie名称是 SESSION

您可以在DSL中配置Spring Session的cookie名称,如下所示:

@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
logoutHandler.setSessionCookieName("SESSION");
return logoutHandler;
}

自定义 OIDC 提供商会话注册表

默认情况下,Spring Security 会在内存中存储 OIDC 提供商会话与客户端会话之间的所有链接。

在某些情况下,比如集群应用程序中,最好将这些内容存储在其他位置,比如数据库中。

你可以通过配置一个自定义的 OidcSessionRegistry 来实现这一点,如下所示:

@Component
public final class MySpringDataOidcSessionRegistry implements OidcSessionRegistry {
private final OidcProviderSessionRepository sessions;

// ...

@Override
public void saveSessionInformation(OidcSessionInformation info) {
this.sessions.save(info);
}

@Override
public OidcSessionInformation removeSessionInformation(String clientSessionId) {
return this.sessions.removeByClientSessionId(clientSessionId);
}

@Override
public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}
java