此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 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

此外,您应该配置OidcClientInitiatedLogoutSuccessHandler,它实施 RP 启动的注销,如下所示:spring-doc.cadn.net.cn

@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,请在请求时替换它。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 SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .oauth2Login(withDefaults())
        .oidcLogout((logout) -> logout
            .backChannel(Customizer.withDefaults())
        );
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        oauth2Login { }
        oidcLogout {
            backChannel { }
        }
    }
    return http.build()
}

然后,您需要一种方法来监听 Spring Security 发布的事件来删除旧的OidcSessionInformation条目,如下所示:spring-doc.cadn.net.cn

@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
@Bean
open fun sessionEventPublisher(): HttpSessionEventPublisher {
    return HttpSessionEventPublisher()
}

这将使如果HttpSession#invalidate,则会话也会从内存中删除。spring-doc.cadn.net.cn

就是这样!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 相关联OidcSessionRegistry实现。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

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

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