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.