OIDC 登出
當終端使用者能夠登入您的應用程式後,考慮他們如何登出也很重要。
一般來說,有三種用例需要您考慮
-
我只想執行本地登出
-
我想由我的應用程式發起,同時登出我的應用程式和 OIDC Provider
-
我想由 OIDC Provider 發起,同時登出我的應用程式和 OIDC Provider
本地登出
要執行本地登出,無需特殊的 OIDC 配置。Spring Security 會自動搭建一個本地登出端點,您可以透過 配置 logout()
DSL 進行配置。
OpenID Connect 1.0 客戶端發起登出
OpenID Connect 會話管理 1.0 允許使用客戶端在 Provider 端登出終端使用者。其中一種可用策略是 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-Initiated Logout 的 OidcClientInitiatedLogoutSuccessHandler
,如下所示
-
Java
-
Kotlin
@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;
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Autowired
private lateinit var clientRegistrationRepository: ClientRegistrationRepository
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
logout {
logoutSuccessHandler = oidcLogoutSuccessHandler()
}
}
return http.build()
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(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 透過向客戶端發起 API 呼叫來在客戶端登出終端使用者。這被稱為 OIDC 後端通道登出。
要啟用此功能,您可以在 DSL 中搭建後端通道登出端點,如下所示
-
Java
-
Kotlin
@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();
}
@Bean
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
return OidcBackChannelLogoutHandler()
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
oidcLogout {
backChannel { }
}
}
return http.build()
}
然後,您需要一種方式來監聽 Spring Security 釋出的事件,以移除舊的 OidcSessionInformation
條目,如下所示
-
Java
-
Kotlin
@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
open fun sessionEventPublisher(): HttpSessionEventPublisher {
return HttpSessionEventPublisher()
}
這將使得如果在呼叫 HttpSession#invalidate
後,該會話也會從記憶體中移除。
就是這樣!
這將搭建端點 /logout/connect/back-channel/{registrationId}
,OIDC Provider 可以透過請求此端點來使您應用程式中給定終端使用者的會話失效。
oidcLogout 要求 oauth2Login 也已配置。 |
oidcLogout 要求會話 cookie 必須命名為 JSESSIONID ,以便通過後端通道正確登出每個會話。 |
後端通道登出架構
考慮一個識別符號為 registrationId
的 ClientRegistration
。
後端通道登出的整體流程如下
-
登入時,Spring Security 在其
OidcSessionRegistry
實現中,將 ID 令牌、CSRF 令牌和 Provider 會話 ID(如有)關聯到您應用程式的會話 ID。 -
然後,在登出時,您的 OIDC Provider 會向
/logout/connect/back-channel/registrationId
發起 API 呼叫,並在其中包含一個 Logout Token,該令牌指示要登出的sub
(終端使用者)或sid
(Provider 會話 ID)。 -
Spring Security 驗證令牌的簽名和宣告。
-
如果令牌包含
sid
宣告,則只有與該 Provider 會話相關的客戶端會話被終止。 -
否則,如果令牌包含
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
-
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 OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry sessionRegistry) { OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry); logoutHandler.setSessionCookieName("SESSION"); return logoutHandler; }
@Bean open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler { val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry) logoutHandler.setSessionCookieName("SESSION") return logoutHandler }
自定義 OIDC Provider 會話登錄檔
預設情況下,Spring Security 在記憶體中儲存 OIDC Provider 會話和客戶端會話之間的所有連結。
在許多情況下,例如叢集應用程式,最好將其儲存在單獨的位置,例如資料庫中。
您可以透過配置自定義 OidcSessionRegistry
來實現此目的,如下所示
-
Java
-
Kotlin
@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(...);
}
}
@Component
class MySpringDataOidcSessionRegistry: OidcSessionRegistry {
val sessions: OidcProviderSessionRepository
// ...
@Override
fun saveSessionInformation(info: OidcSessionInformation) {
this.sessions.save(info)
}
@Override
fun removeSessionInformation(clientSessionId: String): OidcSessionInformation {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
fun removeSessionInformation(token: OidcLogoutToken): Iterable<OidcSessionInformation> {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}