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。您可以透過如下配置帶有 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-發起登出的 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 Session Management 1.0 允許提供程式透過向客戶端發出 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 {
authorizeHttpRequests {
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 提供程式可以請求該端點來使您應用程式中終端使用者的給定會話失效。
oidcLogout 要求 oauth2Login 也已配置。 |
oidcLogout 要求會話 cookie 被命名為 JSESSIONID,以便通過後端通道正確登出每個會話。 |
後端通道登出架構
考慮一個識別符號為 registrationId 的 ClientRegistration。
後端通道登出的總體流程如下:
-
在登入時,Spring Security 將 ID 令牌、CSRF 令牌和提供程式會話 ID(如果有)與您的應用程式會話 ID 在其
OidcSessionRegistry實現中進行關聯。 -
然後在登出時,您的 OIDC 提供程式向
/logout/connect/back-channel/registrationId發出 API 呼叫,其中包括一個登出令牌,該令牌指示要登出的sub(終端使用者)或sid(提供程式會話 ID)。 -
Spring Security 驗證令牌的簽名和宣告。
-
如果令牌包含
sid宣告,則僅終止與該提供程式會話相關的客戶端會話。 -
否則,如果令牌包含
sub宣告,則終止該終端使用者的所有客戶端會話。
請記住,Spring Security 的 OIDC 支援是多租戶的。這意味著它只會終止其客戶端與登出令牌中的 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 oidcSessionRegistry) {
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 提供程式會話登錄檔
預設情況下,Spring Security 在記憶體中儲存 OIDC 提供程式會話和客戶端會話之間的所有連結。
在某些情況下,例如叢集應用程式,將此儲存在單獨的位置(例如資料庫)會更好。
您可以透過配置自定義 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(...);
}
}