OIDC 退出

一旦最终用户能够登录到您的应用程序,考虑他们将如何退出就非常重要。

一般来说,有三种用例需要您考虑

  1. 我只想执行本地退出

  2. 我想同时退出我的应用程序和 OIDC Provider,由我的应用程序发起

  3. 我想同时退出我的应用程序和 OIDC Provider,由 OIDC Provider 发起

本地退出

要执行本地退出,不需要特殊的 OIDC 配置。Spring Security 会自动启动一个本地退出端点,您可以通过 logout() DSL 进行配置

OpenID Connect 1.0 客户端发起退出

OpenID Connect 会话管理 1.0 允许客户端在 Provider 端退出最终用户。其中一种可用的策略是 RP-发起退出(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-发起退出的 OidcClientInitiatedServerLogoutSuccessHandler,如下所示

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

OidcClientInitiatedServerLogoutSuccessHandler 支持 {baseUrl} 占位符。如果使用,应用程序的基础 URL,例如 app.example.org,会在请求时替换它。

OpenID Connect 1.0 后端通道退出

OpenID Connect 会话管理 1.0 允许 Provider 通过向 Client 发起 API 调用来退出 Client 端的最终用户。这被称为 OIDC 后端通道退出(OIDC Back-Channel Logout)

要启用此功能,您可以在 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 Provider 可以请求此端点来使您应用程序中给定最终用户的会话失效。

oidcLogout 要求同时也配置 oauth2Login
oidcLogout 要求会话 cookie 被命名为 JSESSIONID,以便通过后端通道正确地退出每个会话。

后端通道退出架构

考虑一个标识符为 registrationIdClientRegistration

后端通道退出的总体流程如下

  1. 登录时,Spring Security 在其 ReactiveOidcSessionRegistry 实现中将 ID Token、CSRF Token 和 Provider Session ID(如果有)与您应用程序的会话 ID 相关联。

  2. 然后在退出时,您的 OIDC Provider 会向 /logout/connect/back-channel/registrationId 发起 API 调用,其中包含一个 Logout Token,该令牌指示要退出的 sub(最终用户)或 sid(Provider 会话 ID)。

  3. Spring Security 会验证令牌的签名和声明(claims)。

  4. 如果令牌包含 sid 声明,则只有与该 provider 会话相关的 Client 会话会被终止。

  5. 否则,如果令牌包含 sub 声明,则该最终用户的所有 Client 会话都会被终止。

请记住,Spring Security 的 OIDC 支持是多租户的。这意味着它只会终止 Logout Token 中 aud 声明与之匹配的 Client 的会话。

自定义会话退出端点

当发布 OidcBackChannelServerLogoutHandler 后,会话退出端点是 {baseUrl}/logout/connect/back-channel/{registrationId}

如果 OidcBackChannelServerLogoutHandler 未配置,则 URL 为 {baseUrl}/logout/connect/back-channel/{registrationId},但不推荐这样做,因为它需要传递一个 CSRF token,根据您应用程序使用的仓库类型,这可能会很有挑战性。

如果您需要自定义端点,可以按如下方式提供 URL

  • Java

  • Kotlin

http
    // ...
    .oidcLogout((oidc) -> oidc
        .backChannel((backChannel) -> backChannel
            .logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
        )
    );
http {
    oidcLogout {
        backChannel {
            logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
        }
    }
}

默认情况下,会话退出端点使用 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 Provider 会话注册表

默认情况下,Spring Security 在内存中存储 OIDC Provider 会话和 Client 会话之间的所有关联。

在许多情况下,比如集群应用程序,将这些信息存储在单独的位置(例如数据库)中会更好。

您可以通过配置自定义的 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(...);
    }
}