Performing Single Logout

Spring Security 附带了对 RP- 和 AP- 发起的 SAML 2.0 单点注销的支持。 简而言之,Spring Security 支持以下两种用例:

  • RP-Initiated- 您的应用程序有一个端点,在对其执行 POST 操作时,它会注销用户并发送 `saml2:LogoutRequest`给声明方。此后,声明方将回送一个 `saml2:LogoutResponse`并允许您的应用程序响应

  • AP-Initiated- 您的应用程序有一个端点,将会从声明方接收到 saml2:LogoutRequest。那时您的应用程序将完成注销,然后向声明方发送 saml2:LogoutResponse

AP-Initiated 场景中,应用程序在退出后将要执行的任何本地重定向都将变得毫无意义。一旦应用程序发送 saml2:LogoutResponse,它将不再控制浏览器。

Minimal Configuration for Single Logout

要使用 Spring Security 的 SAML 2.0 单点注销功能,您需要以下内容:

  • 首先,声明方必须支持 SAML 2.0 单点注销

  • 其次,声明方应当被配置为对 `saml2:LogoutRequest`和 `saml2:LogoutResponse`进行签名和 POST,对您的应用程序的 `/logout/saml2/slo`端点进行签名和 POST

  • 第三,您的应用程序必须有一个用于签名 `saml2:LogoutRequest`和 `saml2:LogoutResponse`的 PKCS#8 私钥和 X.509 证书

您可以从初始最小示例开始,并添加以下配置:

@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`实例或 multiple instances
2 - 其次,表明您的应用程序希望使用 SAML SLO 来注销最终用户

Runtime Expectations

给定以上配置,任何已登录的用户都可以向您的应用程序发送 POST /logout 来执行 RP 发起的 SLO。然后,您的应用程序将执行以下操作:

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

  2. 使用 Saml2LogoutRequestResolver`创建、签名并序列化与当前登录用户关联的 `<saml2:LogoutRequest>

  3. 根据 RelyingPartyRegistration向声明方发送重定向或帖子

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

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

此外,如果断言方向 /logout/saml2/slo 发送 <saml2:LogoutRequest> 时,您的应用程序也可以参与 AP 发起的注销操作:

  1. 使用 `Saml2LogoutRequestHandler`对声明方发送的 `&lt;saml2:LogoutRequest&gt;`进行反序列化、验证和处理

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

  3. 创建、签名并序列化基于刚注销的用户关联的 RelyingPartyRegistration&lt;saml2:LogoutResponse&gt;

  4. 根据 RelyingPartyRegistration向声明方发送重定向或帖子

添加 saml2Logout 会为服务提供者增加退出功能。由于这是一个可选功能,您需要为每个单独的 RelyingPartyRegistration 启用该功能。您可以通过设置 RelyingPartyRegistration.Builder#singleLogoutServiceLocation 属性来实现。

Configuring Logout Endpoints

有三个行为可由不同的端点触发:

  • RP 发起的注销,允许经过身份验证的用户 `POST`并通过向声明方发送 `&lt;saml2:LogoutRequest&gt;`触发注销过程

  • AP 发起的注销,允许声明方向应用程序发送 &lt;saml2:LogoutRequest&gt;

  • AP 注销响应,它允许声明方对 RP 发起的 &lt;saml2:LogoutRequest&gt;`发送 `&lt;saml2:LogoutResponse&gt;

第一个是在主体类型为 Saml2AuthenticatedPrincipal 时执行常规 POST /logout 触发。

第二个由用宣告方签名的 SAMLRequest POST 到 /logout/saml2/slo 终结点触发。

第三个由用宣告方签名的 SAMLResponse POST 到 /logout/saml2/slo 终结点触发。

因为用户已经登陆或原始的登出请求是已知的,所以 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 中配置这些终结点。

Customizing <saml2:LogoutRequest> Resolution

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

默认情况下,Spring Security 将发出 <saml2:LogoutRequest> 并提供:

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

  • `ID`属性 - GUID

  • &lt;Issuer&gt;`元素 - 来自 `RelyingPartyRegistration#getEntityId

  • &lt;NameID&gt;`元素 - 来自 `Authentication#getName

若要添加其他值,你可以使用委托,如下所示:

@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)
        )
    );

Customizing <saml2:LogoutResponse> Resolution

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

默认情况下,Spring Security 将发出 <saml2:LogoutResponse> 并提供:

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

  • `ID`属性 - GUID

  • &lt;Issuer&gt;`元素 - 来自 `RelyingPartyRegistration#getEntityId

  • &lt;Status&gt;`元素 - `SUCCESS

若要添加其他值,你可以使用委托,如下所示:

@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)
        )
    );

Customizing <saml2:LogoutRequest> Authentication

若要自定义验证,你可以实现你自己的 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)
        )
    );

Customizing <saml2:LogoutResponse> Authentication

若要自定义验证,你可以实现你自己的 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)
        )
    );

Customizing <saml2:LogoutRequest> storage

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

如果你想将在会话之外的某个地方存储登出请求,你可以如此在 DSL 中提供你的自定义实现:

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