执行单次(Single Logout)注销

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

Spring Security支持RP和AP发起的SAML 2.0单一(Single Logout)注销。

简而言之,Spring Security支持两种使用情况。

  • RP发起 - 你的应用程序有一个端点,当被POST到时,将注销用户并发送一个 saml2:LogoutRequest 给断言方。此后,断言方将发回一个 saml2:LogoutResponse,并允许你的应用程序响应

  • AP发起 - 你的应用程序有一个端点,将从断言方收到一个 saml2:LogoutRequest。你的应用程序将在这时完成它的注销,然后发送一个 saml2:LogoutResponse 给断言方。

在AP发起的情况下,你的应用程序在注销后所做的任何本地重定向都将失去意义。一旦你的应用程序发送了一个 saml2:LogoutResponse,它就不再有对浏览器的控制。

单次注销的最小化配置

要使用Spring Security的SAML 2.0单一注销功能,你需要以下东西。

  • 首先,断言方必须支持SAML 2.0单次注销。

  • 第二,断言方应被配置为签署并将 saml2:LogoutRequestsaml2:LogoutResponse POST 到你的应用程序的 /logout/saml2/slo 端点。

  • 第三,你的应用程序必须有一个 PKCS#8 私钥和 X.509 证书来签署 saml2:LogoutRequestsaml2:LogoutResponse

你可以从最初的最小例子开始,添加以下配置。

@Value("${private.key}") RSAPrivateKey key;
@Value("${public.certificate}") X509Certificate certificate;

@Bean
RelyingPartyRegistrationRepository registrations() {
    Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation("https://ap.example.org/metadata")
            .registrationId("id")
            .singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo")
            .signingX509Credentials((signing) -> signing.add(credential)) (1)
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}

@Bean
SecurityFilterChain web(HttpSecurity http, RelyingPartyRegistrationRepository registrations) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults())
        .saml2Logout(withDefaults()); (2)

    return http.build();
}
1 首先,将你的签名密钥添加到 RelyingPartyRegistration 实例或 多个实例中。
2 第二,表明你的应用程序要使用SAML SLO来注销终端用户。

Runtime Expectation

鉴于上述配置,任何登录的用户都可以向你的应用程序发送一个 POST /logout,以执行RP发起的SLO。然后你的应用程序将做以下工作。

  1. 注销用户并使会话无效。

  2. 使用 Saml2LogoutRequestResolver 来创建、签署和序列化一个 <saml2:LogoutRequest>,基于与当前登录的用户相关的 RelyingPartyRegistration

  3. 根据 RelyingPartyRegistration,向断言方发送一个重定向(redirect)或post。

  4. 对断言方发送的 <saml2:LogoutResponse> 进行反序列化、验证和处理。

  5. 重定向到任何配置好的成功注销端点。

另外,当断言方发送 <saml2:LogoutRequest>/logout/saml2/slo 时,你的应用程序可以参与AP发起的注销。

  1. 使用 Saml2LogoutRequestHandler 来反序列化、验证和处理断言方发送的 <saml2:LogoutRequest>

  2. 注销用户并使会话无效。

  3. 创建、签署和序列化一个 <saml2:LogoutResponse>,基于与刚登出的用户相关的 RelyingPartyRegistration

  4. 根据 RelyingPartyRegistration,向断言方发送一个redirect或POST。

添加 saml2Logout 可以为服务提供者增加注销的能力。因为它是一个可选的能力,你需要为每个独立的 RelyingPartyRegistration 启用它。你可以通过设置 RelyingPartyRegistration.Builder#singleLogoutServiceLocation 属性来实现。

配置注销端点

有三种行为可以由不同的端点触发。

  • RP发起的注销,允许认证的用户通过向断言方发送 <saml2:LogoutRequest>POST 并触发注销过程。

  • AP发起的注销,允许断言方向应用程序发送 <saml2:LogoutRequest>

  • AP注销响应,允许断言方发送 <saml2:LogoutResponse> 以响应RP发起的 <saml2:LogoutRequest>

当委托人(principal)是 Saml2AuthenticatedPrincipal 类型时,第一个是通过执行正常的 POST /logout 来触发的。

第二种是通过向 /logout/saml2/slo 端点发送由断言方签名的 SAMLRequest 来触发。

第三种是通过向 /logout/saml2/slo 端点发送由断言方签名的 SAMLResponse 来触发的。

因为用户已经登录或者原始的注销请求是已知的,所以 registrationId 已经是已知的。出于这个原因,{registrationId} 默认不是这些URL的一部分。

这个URL在DSL中是可以定制的。

例如,如果你正在将现有的依赖方迁移到 Spring Security,你的断言方可能已经指向 GET /SLOService.saml2。为了减少断言方的配置变化,你可以在DSL中这样配置过滤器。

  • Java

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
        .logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
    );

你也应该在你的 RelyingPartyRegistration 中配置这些端点。

自定义 <saml2:LogoutRequest> 解析

<saml2:LogoutRequest> 中,除了 Spring Security 提供的默认值外,通常需要设置其他值。

默认情况下,Spring Security 会发布一个 <saml2:LogoutRequest> 并提供。

  • Destination 属性 - 来自 RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation

  • ID 属性 - 一个GUID

  • <Issuer> 元素 - 来自 RelyingPartyRegistration#getEntityId

  • <NameID> 元素 - 来自 Authentication#getName

要添加其他值,你可以使用委托(delegation),像这样。

@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
	OpenSaml4LogoutRequestResolver logoutRequestResolver =
			new OpenSaml4LogoutRequestResolver(registrations);
	logoutRequestResolver.setParametersConsumer((parameters) -> {
		String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
		String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
		LogoutRequest logoutRequest = parameters.getLogoutRequest();
		NameID nameId = logoutRequest.getNameID();
		nameId.setValue(name);
		nameId.setFormat(format);
	});
	return logoutRequestResolver;
}

然后,你可以在DSL中提供你自定义的 Saml2LogoutRequestResolver,如下所示。

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestResolver(this.logoutRequestResolver)
        )
    );

自定义 <saml2:LogoutResponse> 解析

<saml2:LogoutResponse> 中,除了Spring Security 提供的默认值外,通常需要设置其他值。

默认情况下,Spring Security会发布一个 <saml2:LogoutResponse> 并提供:

  • Destination 属性 - 来自 RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation

  • ID 属性 - GUID

  • <Issuer> 元素 - 来自 RelyingPartyRegistration#getEntityId

  • <Status> 元素 - SUCCESS

要添加其他值,你可以使用委托(delegation),像这样。

@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
	OpenSaml4LogoutResponseResolver logoutRequestResolver =
			new OpenSaml4LogoutResponseResolver(registrations);
	logoutRequestResolver.setParametersConsumer((parameters) -> {
		if (checkOtherPrevailingConditions(parameters.getRequest())) {
			parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
		}
	});
	return logoutRequestResolver;
}

然后,你可以在DSL中提供你自定义的 Saml2LogoutResponseResolver,如下所示。

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestResolver(this.logoutRequestResolver)
        )
    );

自定义 <saml2:LogoutRequest> 认证

为了定制认证,你可以实现你自己的 Saml2LogoutRequestValidator。在这一点上,验证是最小的,所以你可以先委托给默认的 Saml2LogoutRequestValidator,像这样。

@Component
public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
	private final Saml2LogoutRequestValidator delegate = new OpenSamlLogoutRequestValidator();

	@Override
    public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
		 // verify signature, issuer, destination, and principal name
		Saml2LogoutValidatorResult result = delegate.authenticate(authentication);

		LogoutRequest logoutRequest = // ... parse using OpenSAML
        // perform custom validation
    }
}

然后,你可以在DSL中提供你自定义的 Saml2LogoutRequestValidator,如下所示。

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestAuthenticator(myOpenSamlLogoutRequestAuthenticator)
        )
    );

自定义 <saml2:LogoutResponse> 认证

为了定制验证,你可以实现你自己的 Saml2LogoutResponseValidator。在这一点上,验证是最小的,所以你可以先委托给默认的 Saml2LogoutResponseValidator,像这样。

@Component
public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
	private final Saml2LogoutResponseValidator delegate = new OpenSamlLogoutResponseValidator();

	@Override
    public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
		// verify signature, issuer, destination, and status
		Saml2LogoutValidatorResult result = delegate.authenticate(parameters);

		LogoutResponse logoutResponse = // ... parse using OpenSAML
        // perform custom validation
    }
}

然后,你可以在DSL中提供你自定义的 Saml2LogoutResponseValidator,如下所示。

http
    .saml2Logout((saml2) -> saml2
        .logoutResponse((response) -> response
            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
        )
    );

自定义 <saml2:LogoutRequest> 存储

当你的应用程序发送 <saml2:LogoutRequest> 时,该值被存储在会话中,以便 <saml2:LogoutResponse> 中的 RelayState 参数和 InResponseTo 属性可以被验证。

如果你想在会话之外的其他地方存储注销请求,你可以在DSL中提供你的自定义实现,像这样。

http
    .saml2Logout((saml2) -> saml2
        .logoutRequest((request) -> request
            .logoutRequestRepository(myCustomLogoutRequestRepository)
        )
    );