OIDC 退出
一旦最终用户能够登录到您的应用程序,考虑他们将如何退出就非常重要。
一般来说,有三种用例需要您考虑
-
我只想执行本地退出
-
我想同时退出我的应用程序和 OIDC Provider,由我的应用程序发起
-
我想同时退出我的应用程序和 OIDC Provider,由 OIDC Provider 发起
本地退出
要执行本地退出,不需要特殊的 OIDC 配置。Spring Security 会自动启动一个本地退出端点,您可以通过 logout()
DSL 进行配置。
OpenID Connect 1.0 客户端发起退出
OpenID Connect 会话管理 1.0 允许客户端在 Provider 端退出最终用户。其中一种可用的策略是 RP-发起退出(RP-Initiated Logout)。
如果 OpenID Provider 同时支持会话管理和 发现(Discovery),客户端可以从 OpenID Provider 的 发现元数据(Discovery Metadata)中获取 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
此外,您还应该配置实现 RP-发起退出的 OidcClientInitiatedServerLogoutSuccessHandler
,如下所示
-
Java
-
Kotlin
@Configuration
@EnableWebFluxSecurity
public class OAuth2LoginSecurityConfig {
@Autowired
private ReactiveClientRegistrationRepository clientRegistrationRepository;
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2Login(withDefaults())
.logout((logout) -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
return http.build();
}
private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedServerLogoutSuccessHandler(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;
}
}
@Configuration
@EnableWebFluxSecurity
class OAuth2LoginSecurityConfig {
@Autowired
private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository
@Bean
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login { }
logout {
logoutSuccessHandler = oidcLogoutSuccessHandler()
}
}
return http.build()
}
private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler {
val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(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
}
}
|
OpenID Connect 1.0 后端通道退出
OpenID Connect 会话管理 1.0 允许 Provider 通过向 Client 发起 API 调用来退出 Client 端的最终用户。这被称为 OIDC 后端通道退出(OIDC Back-Channel Logout)。
要启用此功能,您可以在 DSL 中配置后端通道退出端点,如下所示
-
Java
-
Kotlin
@Bean
OidcBackChannelServerLogoutHandler oidcLogoutHandler() {
return new OidcBackChannelServerLogoutHandler();
}
@Bean
public SecurityWebFilterChain filterChain(ServerHttpSecurity http) throws Exception {
http
.authorizeExchange((authorize) -> authorize
.anyExchange().authenticated()
)
.oauth2Login(withDefaults())
.oidcLogout((logout) -> logout
.backChannel(Customizer.withDefaults())
);
return http.build();
}
@Bean
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
return OidcBackChannelLogoutHandler()
}
@Bean
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http {
authorizeExchange {
authorize(anyExchange, authenticated)
}
oauth2Login { }
oidcLogout {
backChannel { }
}
}
return http.build()
}
就这样!
这将启动端点 /logout/connect/back-channel/{registrationId}
,OIDC Provider 可以请求此端点来使您应用程序中给定最终用户的会话失效。
oidcLogout 要求同时也配置 oauth2Login 。 |
oidcLogout 要求会话 cookie 被命名为 JSESSIONID ,以便通过后端通道正确地退出每个会话。 |
后端通道退出架构
考虑一个标识符为 registrationId
的 ClientRegistration
。
后端通道退出的总体流程如下
-
登录时,Spring Security 在其
ReactiveOidcSessionRegistry
实现中将 ID Token、CSRF Token 和 Provider Session ID(如果有)与您应用程序的会话 ID 相关联。 -
然后在退出时,您的 OIDC Provider 会向
/logout/connect/back-channel/registrationId
发起 API 调用,其中包含一个 Logout Token,该令牌指示要退出的sub
(最终用户)或sid
(Provider 会话 ID)。 -
Spring Security 会验证令牌的签名和声明(claims)。
-
如果令牌包含
sid
声明,则只有与该 provider 会话相关的 Client 会话会被终止。 -
否则,如果令牌包含
sub
声明,则该最终用户的所有 Client 会话都会被终止。
请记住,Spring Security 的 OIDC 支持是多租户的。这意味着它只会终止 Logout Token 中 aud 声明与之匹配的 Client 的会话。 |
自定义会话退出端点
当发布 OidcBackChannelServerLogoutHandler
后,会话退出端点是 {baseUrl}/logout/connect/back-channel/{registrationId}
。
如果 OidcBackChannelServerLogoutHandler
未配置,则 URL 为 {baseUrl}/logout/connect/back-channel/{registrationId}
,但不推荐这样做,因为它需要传递一个 CSRF token,根据您应用程序使用的仓库类型,这可能会很有挑战性。
如果您需要自定义端点,可以按如下方式提供 URL
-
Java
-
Kotlin
http // ... .oidcLogout((oidc) -> oidc .backChannel((backChannel) -> backChannel .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+") ) );
http { oidcLogout { backChannel { logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+" } } }
自定义会话退出 Cookie 名称
默认情况下,会话退出端点使用 JSESSIONID
cookie 来关联会话与相应的 OidcSessionInformation
。
然而,Spring Session 中的默认 cookie 名称是 SESSION
。
您可以在 DSL 中像这样配置 Spring Session 的 cookie 名称
-
Java
-
Kotlin
@Bean OidcBackChannelServerLogoutHandler oidcLogoutHandler(ReactiveOidcSessionRegistry sessionRegistry) { OidcBackChannelServerLogoutHandler logoutHandler = new OidcBackChannelServerLogoutHandler(sessionRegistry); logoutHandler.setSessionCookieName("SESSION"); return logoutHandler; }
@Bean open fun oidcLogoutHandler(val sessionRegistry: ReactiveOidcSessionRegistry): OidcBackChannelServerLogoutHandler { val logoutHandler = OidcBackChannelServerLogoutHandler(sessionRegistry) logoutHandler.setSessionCookieName("SESSION") return logoutHandler }
自定义 OIDC Provider 会话注册表
默认情况下,Spring Security 在内存中存储 OIDC Provider 会话和 Client 会话之间的所有关联。
在许多情况下,比如集群应用程序,将这些信息存储在单独的位置(例如数据库)中会更好。
您可以通过配置自定义的 ReactiveOidcSessionRegistry
来实现这一点,如下所示
-
Java
-
Kotlin
@Component
public final class MySpringDataOidcSessionRegistry implements ReactiveOidcSessionRegistry {
private final OidcProviderSessionRepository sessions;
// ...
@Override
public Mono<void> saveSessionInformation(OidcSessionInformation info) {
return this.sessions.save(info);
}
@Override
public Mono<OidcSessionInformation> removeSessionInformation(String clientSessionId) {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
public Flux<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}
@Component
class MySpringDataOidcSessionRegistry: ReactiveOidcSessionRegistry {
val sessions: OidcProviderSessionRepository
// ...
@Override
fun saveSessionInformation(info: OidcSessionInformation): Mono<Void> {
return this.sessions.save(info)
}
@Override
fun removeSessionInformation(clientSessionId: String): Mono<OidcSessionInformation> {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
fun removeSessionInformation(token: OidcLogoutToken): Flux<OidcSessionInformation> {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}