# 执行单注销

Spring 安全性支持RP和AP发起的SAML2.0单注销。

简单地说，有两个用例 Spring 安全支持：

* **RP-initiated **-你的应用程序有一个端点，当发送到该端点时，该端点将注销该用户并向断言的一方发送`saml2:LogoutRequest`。在此之后，断言方会返回一个`saml2:LogoutResponse`并允许你的应用程序进行响应

* **AP-initied **-你的应用程序有一个端点，它将从主张方接收`saml2:LogoutRequest`。你的应用程序将在此时完成注销，然后向断言的一方发送`saml2:LogoutResponse`。

|   |在**AP-initied **场景中，应用程序在注销后进行的任何本地重定向都是没有意义的。<br/>一旦应用程序发送了`saml2:LogoutResponse`，它就不再具有对浏览器的控制权。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 单注销的最小配置

要使用 Spring Security的SAML2.0单注销功能，你将需要以下功能：

* 首先，断言一方必须支持SAML2.0单注销

* 其次，应该将断言一方配置为签名并发布`saml2:LogoutRequest`s和`saml2:LogoutResponse`s你的应用程序的`/logout/saml2/slo`端点

* 第三，你的应用程序必须具有用于签名`saml2:LogoutRequest`s和`saml2:LogoutResponse`s的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`实例或[多个实例](login/overview.html#servlet-saml2login-rpr-duplicated)实例|
|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|\-其次，指示你的应用程序想要使用SAML SLO注销最终用户|

### 运行时期望

给定上述配置，任何登录的用户都可以向你的应用程序发送`POST /logout`，以执行RP发起的SLO。然后，你的应用程序将执行以下操作：

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

2. 使用`Saml2LogoutRequestResolver`来创建、签名和序列化基于与当前登录用户关联的[`RelyingPartyRegistration`]（login/overview.html# Servlet-saml2login-relyingpartyregistration）的`<saml2:LogoutRequest>`。

3. 根据[`RelyingPartyRegistration`]（login/overview.html# Servlet-saml2login-relyingpartyregistration）向断言的一方发送重定向或帖子

4. 反序列化、验证和处理由断言方发送的`<saml2:LogoutResponse>`

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

此外，当断言一方向`<saml2:LogoutRequest>`发送`/logout/saml2/slo`时，你的应用程序可以参与AP发起的注销：

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

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

3. 基于与刚刚注销的用户关联的[`RelyingPartyRegistration`]（login/overview.html# Servlet-saml2login-relyingpartyregistration），创建、签名和序列化`<saml2:LogoutResponse>`

4. 基于[`RelyingPartyRegistration`]（login/overview.html# Servlet-saml2login-relyingpartyregistration）向断言方发一个重定向或发布

|   |添加`saml2Logout`会将注销的功能添加到服务提供程序。<br/>由于它是一种可选功能，因此你需要为每个单独的`RelyingPartyRegistration`启用它。<br/>你可以通过设置`RelyingPartyRegistration.Builder#singleLogoutServiceLocation`属性来实现这一点。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 配置注销端点

不同的端点可以触发三种行为：

* RP-initiated logout，它允许经过身份验证的用户`POST`，并通过向断言的一方发送`<saml2:LogoutRequest>`来触发注销过程。

* AP发起的注销，它允许断言的一方向应用程序发送`<saml2:LogoutRequest>`

* AP注销响应，它允许断言一方发送`<saml2:LogoutResponse>`作为对RP发起的`<saml2:LogoutRequest>`的响应

第一种是当主体类型`Saml2AuthenticatedPrincipal`时，执行正常的`POST /logout`触发。

第二种是通过发送到带有由主张方签名的`SAMLRequest`的`/logout/saml2/slo`端点来触发的。

第三种是通过发送到带有由主张方签名的`SAMLResponse`的`/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`中配置这些端点。

## 自定义`<saml2:LogoutRequest>`分辨率

通常需要在`<saml2:LogoutRequest>`中设置其他值，而不是 Spring Security提供的默认值。

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

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

* `ID`属性-guid

* `<Issuer>`元素-来自`RelyingPartyRegistration#getEntityId`

* `<NameID>`元素-来自`Authentication#getName`

要添加其他值，可以使用委托，例如：

```
@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationResolver registrationResolver) {
	OpenSaml4LogoutRequestResolver logoutRequestResolver
			new OpenSaml4LogoutRequestResolver(registrationResolver);
	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`

要添加其他值，可以使用委托，例如：

```
@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationResolver registrationResolver) {
	OpenSaml4LogoutResponseResolver logoutRequestResolver =
			new OpenSaml3LogoutResponseResolver(relyingPartyRegistrationResolver);
	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>`时，该值存储在会话中，以便`RelayState`参数和`InResponseTo`属性中的`<saml2:LogoutResponse>`可以被验证。

如果你希望将注销请求存储在会话以外的某个地方，那么可以在DSL中提供你的自定义实现，如下所示：

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

[SAML2身份验证响应](login/authentication.html)[SAML2元数据](metadata.html)