OIDC 登出
一旦最终用户能够登录到您的应用程序,就需要考虑他们如何注销。
一般来说,您需要考虑以下三种使用场景:
-
我只想执行本地注销
-
我希望由我的应用程序发起,同时注销我的应用程序和 OIDC 提供商
-
我希望由 OIDC 提供商发起,同时注销我的应用程序和 OIDC 提供商
本地登出
要执行本地注销,无需特殊的 OIDC 配置。
Spring Security 会自动提供一个本地注销端点,您可以通过 logout() DSL 进行配置。
OpenID Connect 1.0 客户端发起的注销
OpenID Connect 会话管理 1.0 允许通过客户端(Client)将最终用户从提供者(Provider)处注销。 其中一种可用的策略是RP 发起的注销(RP-Initiated Logout)。
如果 OpenID 提供商同时支持会话管理(Session Management)和发现(Discovery),客户端可以从 OpenID 提供商的发现元数据(Discovery Metadata)中获取 URL 的 https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata。
您可以通过使用 ClientRegistration 配置 issuer-uri 来实现这一点,如下所示:
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
此外,您还应配置 OidcClientInitiatedServerLogoutSuccessHandler,该类实现了 RP 发起的注销(RP-Initiated Logout),配置方式如下:
-
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 调用,从而在客户端注销最终用户。 这被称为OIDC 后端通道登出(OIDC Back-Channel Logout)。
要启用此功能,您可以在 DSL 中按如下方式设置 Back-Channel Logout(后端通道登出)端点:
-
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 提供商请求以使您应用程序中指定终端用户的会话失效。
oidcLogout 要求同时配置 oauth2Login。 |
oidcLogout 要求会话 cookie 的名称必须为 JSESSIONID,以便通过后端通道正确注销每个会话。 |
后通道登出架构
考虑一个标识符为 ClientRegistration 的 registrationId。
后端通道(Back-Channel)注销的整体流程如下所示:
-
在登录时,Spring Security 会将其
ReactiveOidcSessionRegistry实现中将 ID Token、CSRF Token 和提供者会话 ID(如果有的话)与您的应用程序会话 ID 进行关联。 -
然后在注销时,您的 OIDC 提供商会调用
/logout/connect/back-channel/registrationId接口,并附带一个注销Tokens(Logout Token),该Tokens指明要注销的用户sub(最终用户)或sid(提供商会话 ID)。 -
Spring Security 验证Tokens的签名和声明。
-
如果Tokens包含
sid声明,则仅终止与该提供者会话相关联的客户端会话。 -
否则,如果Tokens包含
sub声明,则该终端用户的所有客户端会话都将被终止。
请记住,Spring Security 的 OIDC 支持是多租户的。
这意味着它只会终止那些客户端与注销Tokens(Logout Token)中的 aud 声明相匹配的会话。 |
自定义会话登出端点
发布 OidcBackChannelServerLogoutHandler 后,会话注销端点为 {baseUrl}/logout/connect/back-channel/{registrationId}。
如果未连接 OidcBackChannelServerLogoutHandler,那么 URL 是 {baseUrl}/logout/connect/back-channel/{registrationId},这不被推荐,因为需要传递一个 CSRF Tokens,这取决于应用程序使用的仓库类型可能会比较困难。
如果你需要自定义端点,可以按如下方式提供 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 提供商会话注册表
默认情况下,Spring Security 会在内存中存储 OIDC 提供商会话与客户端会话之间的所有关联。
在某些情况下(例如集群应用程序),将此内容存储在单独的位置(如数据库)中会更为理想。
你可以通过配置一个自定义的 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(...);
}
}