OIDC 登出

一旦終端使用者能夠登入到您的應用程式,考慮他們如何登出就變得很重要。

一般來說,您需要考慮三種用例

  1. 我只想執行本地登出

  2. 我想同時登出我的應用程式和 OIDC 提供程式,由我的應用程式發起

  3. 我想同時登出我的應用程式和 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-uriClientRegistration 來實現:

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
    }
}

OidcClientInitiatedLogoutSuccessHandler 支援 {baseUrl} 佔位符。如果使用,應用程式的基本 URL,例如 app.example.org,將在請求時替換它。

預設情況下,OidcClientInitiatedLogoutSuccessHandler 使用帶有 GET 方法的標準 HTTP 重定向來重定向到登出 URL。要使用 POST 請求執行登出,請將重定向策略設定為 FormPostRedirectStrategy,例如使用 OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormPostRedirectStrategy())

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,以便通過後端通道正確登出每個會話。

後端通道登出架構

考慮一個識別符號為 registrationIdClientRegistration

後端通道登出的總體流程如下:

  1. 在登入時,Spring Security 將 ID 令牌、CSRF 令牌和提供程式會話 ID(如果有)與您的應用程式會話 ID 在其 OidcSessionRegistry 實現中進行關聯。

  2. 然後在登出時,您的 OIDC 提供程式向 /logout/connect/back-channel/registrationId 發出 API 呼叫,其中包括一個登出令牌,該令牌指示要登出的 sub(終端使用者)或 sid(提供程式會話 ID)。

  3. Spring Security 驗證令牌的簽名和宣告。

  4. 如果令牌包含 sid 宣告,則僅終止與該提供程式會話相關的客戶端會話。

  5. 否則,如果令牌包含 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}+"
        }
    }
}

預設情況下,會話登出端點使用 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(...);
    }
}
© . This site is unofficial and not affiliated with VMware.