OIDC 登出
一旦終端使用者能夠登入到您的應用程式,考慮他們將如何登出就很重要。
通常來說,有三種用例需要您考慮:
-
我只想執行本地登出
-
我想同時登出我的應用程式和 OIDC 提供者,由我的應用程式發起
-
我想同時登出我的應用程式和 OIDC 提供者,由 OIDC 提供者發起
本地登出
要執行本地登出,不需要特殊的 OIDC 配置。Spring Security 會自動建立一個本地登出端點,您可以透過logout() DSL 進行配置。
OpenID Connect 1.0 客戶端發起登出
OpenID Connect Session Management 1.0 允許客戶端在提供者處登出終端使用者。其中一種可用策略是RP-發起登出。
如果 OpenID 提供者同時支援會話管理和發現,客戶端可以從 OpenID 提供者的發現元資料中獲取 end_session_endpoint URL。您可以透過配置 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-發起登出,如下所示:
-
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 Session Management 1.0 允許提供者透過向客戶端發出 API 呼叫來登出客戶端的終端使用者。這被稱為OIDC 後端通道登出。
要啟用此功能,您可以在 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 提供者可以請求該端點來使您應用程式中給定終端使用者的會話失效。
oidcLogout 要求也配置 oauth2Login。 |
oidcLogout 要求會話 cookie 被命名為 JSESSIONID,以便通過後端通道正確登出每個會話。 |
後端通道登出架構
考慮一個識別符號為 registrationId 的 ClientRegistration。
後端通道登出的總體流程如下:
-
在登入時,Spring Security 將 ID 令牌、CSRF 令牌和提供者會話 ID(如果有)與您應用程式的會話 ID 關聯在其
ReactiveOidcSessionRegistry實現中。 -
然後在登出時,您的 OIDC 提供者向
/logout/connect/back-channel/registrationId發出 API 呼叫,其中包含一個登出令牌,指示要登出的sub(終端使用者)或sid(提供者會話 ID)。 -
Spring Security 驗證令牌的簽名和宣告。
-
如果令牌包含
sid宣告,則只會終止與該提供者會話相關的客戶端會話。 -
否則,如果令牌包含
sub宣告,則將終止該終端使用者的所有客戶端會話。
請記住,Spring Security 的 OIDC 支援是多租戶的。這意味著它只會終止其客戶端與登出令牌中的 aud 宣告匹配的會話。 |
自定義會話登出端點
釋出 OidcBackChannelServerLogoutHandler 後,會話登出端點為 {baseUrl}/logout/connect/back-channel/{registrationId}。
如果未配置 OidcBackChannelServerLogoutHandler,則 URL 為 {baseUrl}/logout/connect/back-channel/{registrationId},不建議這樣做,因為它需要傳遞 CSRF 令牌,這可能根據您的應用程式使用的儲存庫型別而具有挑戰性。
如果您需要自定義端點,可以按如下方式提供 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 提供者會話登錄檔
預設情況下,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(...);
}
}