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("https://:9000/logout/connect/back-channel/+{registrationId}+") ) );
http { oidcLogout { backChannel { logoutUri = "https://: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(...);
}
}