此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.1spring-doc.cadn.net.cn

OIDC 注销

一旦最终用户能够登录到您的应用程序,考虑他们将如何注销非常重要。spring-doc.cadn.net.cn

一般来说,有三个用例供您考虑:spring-doc.cadn.net.cn

  1. 我只想执行本地注销spring-doc.cadn.net.cn

  2. 我想注销我的应用程序和由我的应用程序启动的 OIDC 提供商spring-doc.cadn.net.cn

  3. 我想注销我的应用程序和由 OIDC 提供商发起的 OIDC 提供商spring-doc.cadn.net.cn

本地注销

要执行本地注销,不需要特殊的 OIDC 配置。 Spring Security 会自动建立本地注销端点,您可以通过logout()DSL (英语).spring-doc.cadn.net.cn

OpenID Connect 1.0 客户端发起的注销

OpenID Connect 会话管理 1.0 允许使用客户端在提供程序处注销最终用户。 可用的策略之一是 RP 发起的注销spring-doc.cadn.net.cn

如果 OpenID Provider 同时支持 Session Management 和 Discovery,则客户端可以获取end_session_endpoint URL从 OpenID 提供程序的发现元数据。 为此,您可以配置ClientRegistration使用issuer-uri如下:spring-doc.cadn.net.cn

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 启动的注销,如下所示:spring-doc.cadn.net.cn

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

OidcClientInitiatedServerLogoutSuccessHandler支持{baseUrl}占 位 符。 如果使用,则应用程序的基 URL(例如app.example.org,请在请求时替换它。spring-doc.cadn.net.cn

OpenID Connect 1.0 反向通道注销

OpenID Connect 会话管理 1.0 允许通过让提供商对客户端进行 API 调用,在客户端注销最终用户。 这称为 OIDC 反向通道注销spring-doc.cadn.net.cn

要启用此功能,您可以在 DSL 中建立 Back-Channel Logout 终端节点,如下所示:spring-doc.cadn.net.cn

@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
open fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
    http {
        authorizeExchange {
            authorize(anyExchange, authenticated)
        }
        oauth2Login { }
        oidcLogout {
            backChannel { }
        }
    }
    return http.build()
}

就是这样!spring-doc.cadn.net.cn

这将建立端点/logout/connect/back-channel/{registrationId}OIDC 提供商可以请求使应用程序中最终用户的给定会话失效。spring-doc.cadn.net.cn

oidcLogout要求oauth2Login也被配置。
oidcLogout要求调用会话 CookieJSESSIONID以便通过 BackChannel 正确注销每个会话。

反向通道注销架构

考虑一个ClientRegistration其标识符为registrationId.spring-doc.cadn.net.cn

Back-Channel 注销的总体流程如下:spring-doc.cadn.net.cn

  1. 在登录时, Spring Security 将 ID 令牌、CSRF 令牌和提供者会话 ID(如果有)与其应用程序中的会话 ID 相关联ReactiveOidcSessionRegistry实现。spring-doc.cadn.net.cn

  2. 然后,在注销时,您的 OIDC 提供商会对/logout/connect/back-channel/registrationId包括一个 Logout Token,该令牌指示sub(最终用户)或sid(Provider Session ID) 注销。spring-doc.cadn.net.cn

  3. Spring Security 验证令牌的签名和声明。spring-doc.cadn.net.cn

  4. 如果令牌包含sidclaim,则仅终止与该 provider 会话相关的 Client 会话。spring-doc.cadn.net.cn

  5. 否则,如果令牌包含subclaim,则该 Client 对该 End User 的所有会话都将终止。spring-doc.cadn.net.cn

请记住,Spring Security 的 OIDC 支持是多租户的。 这意味着它只会终止 Client 与audclaim 的 Token。

自定义 OIDC Provider Session Registry

默认情况下,Spring Security 将 OIDC Provider 会话和 Client 会话之间的所有链接存储在内存中。spring-doc.cadn.net.cn

在许多情况下,例如集群应用程序,最好将其存储在单独的位置(如数据库)中。spring-doc.cadn.net.cn

您可以通过配置自定义ReactiveOidcSessionRegistry这样:spring-doc.cadn.net.cn

@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(...);
    }
}