# 验证`<saml2:Response>`s

为了验证SAML2.0响应， Spring Security默认使用[`OpenSaml4AuthenticationProvider`]（overview.html# Servlet-saml2login-architecture）。

你可以通过多种方式配置此功能，包括：

1. 设置时钟倾斜以进行时间戳验证

2. 将响应映射到`GrantedAuthority`实例的列表

3. 自定义验证断言的策略

4. 自定义解密响应和断言元素的策略

要配置这些，你可以在DSL中使用`saml2Login#authenticationManager`方法。

## 设置时钟倾斜

主张和依赖方的系统时钟AREN不能完全同步，这并不罕见。出于这个原因，你可以在一定程度上配置`OpenSaml4AuthenticationProvider`的默认断言验证器：

Java

```
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(assertionToken -> {
                    Map<String, Object> params = new HashMap<>();
                    params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
                    // ... other validation parameters
                    return new ValidationContext(params);
                })
        );

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
    }
}
```

Kotlin

```
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setAssertionValidator(
            OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
                    val params: MutableMap<String, Any> = HashMap()
                    params[CLOCK_SKEW] =
                        Duration.ofMinutes(10).toMillis()
                    ValidationContext(params)
                })
        )
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
    }
}
```

## 与`UserDetailsService`协调

或者，你可能希望包含来自遗留的`UserDetailsService`的用户详细信息。在这种情况下，响应身份验证转换器可以派上用场，如下所示：

Java

```
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
            Saml2Authentication authentication = OpenSaml4AuthenticationProvider
                    .createDefaultResponseAuthenticationConverter() (1)
                    .convert(responseToken);
            Assertion assertion = responseToken.getResponse().getAssertions().get(0);
            String username = assertion.getSubject().getNameID().getValue();
            UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
            return MySaml2Authentication(userDetails, authentication); (3)
        });

        http
            .authorizeHttpRequests(authz -> authz
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(new ProviderManager(authenticationProvider))
            );
    }
}
```

Kotlin

```
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    @Autowired
    var userDetailsService: UserDetailsService? = null

    override fun configure(http: HttpSecurity) {
        val authenticationProvider = OpenSaml4AuthenticationProvider()
        authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
            val authentication = OpenSaml4AuthenticationProvider
                .createDefaultResponseAuthenticationConverter() (1)
                .convert(responseToken)
            val assertion: Assertion = responseToken.response.assertions[0]
            val username: String = assertion.subject.nameID.value
            val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
            MySaml2Authentication(userDetails, authentication) (3)
        }
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = ProviderManager(authenticationProvider)
            }
        }
    }
}
```

|**1**|首先，调用默认的转换器，它从响应中提取属性和权限。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|其次，使用相关信息调用[`UserDetailsService`]（../../身份验证/密码/user-details-service.html# Servlet-身份验证-userdetailsService）|
|**3**|第三，返回包含用户详细信息的自定义身份验证|

|   |它不需要调用`OpenSaml4AuthenticationProvider`的默认身份验证转换器。<br/>它返回一个`Saml2AuthenticatedPrincipal`，其中包含它从`AttributeStatement`s中提取的属性以及单个`ROLE_USER`权限。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 执行额外的响应验证

`OpenSaml4AuthenticationProvider`在解密`Response`后立即验证`Issuer`和`Destination`值。你可以通过扩展连接到你自己的响应验证器的默认验证器来定制验证，也可以用你自己的响应验证器完全替换它。

例如，你可以抛出带有`Response`对象中可用的任何附加信息的自定义异常，例如：

```
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
	Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
		.createDefaultResponseValidator()
		.convert(responseToken)
		.concat(myCustomValidator.convert(responseToken));
	if (!result.getErrors().isEmpty()) {
		String inResponseTo = responseToken.getInResponseTo();
		throw new CustomSaml2AuthenticationException(result, inResponseTo);
	}
	return result;
});
```

## 执行附加的断言验证

`OpenSaml4AuthenticationProvider`对SAML2.0断言执行最小的验证。在验证签名之后，它将：

1. 验证`<AudienceRestriction>`和`<DelegationRestriction>`条件

2. 验证`<SubjectConfirmation>`s，期望获得任何IP地址信息

要执行额外的验证，你可以配置自己的断言验证器，将其委托给`OpenSaml4AuthenticationProvider`的默认值，然后执行自己的断言验证器。

例如，你可以使用OpenSAML的`OneTimeUseConditionValidator`来验证`<OneTimeUse>`条件，如下所示：

Java

```
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
    Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
            .createDefaultAssertionValidator()
            .convert(assertionToken);
    Assertion assertion = assertionToken.getAssertion();
    OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
    ValidationContext context = new ValidationContext();
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            return result;
        }
    } catch (Exception e) {
        return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
    }
    return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
```

Kotlin

```
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
    val result = OpenSaml4AuthenticationProvider
        .createDefaultAssertionValidator()
        .convert(assertionToken)
    val assertion: Assertion = assertionToken.assertion
    val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
    val context = ValidationContext()
    try {
        if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
            [email protected] result
        }
    } catch (e: Exception) {
        [email protected] result.concat(Saml2Error(INVALID_ASSERTION, e.message))
    }
    result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
```

|   |虽然推荐使用，但没有必要调用`OpenSaml4AuthenticationProvider`的默认断言验证器。<br/>如果你不需要它来检查`<AudienceRestriction>`或`<SubjectConfirmation>`，那么你就可以跳过它，因为你是在自己执行这些操作。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 自定义解密

Spring 安全解密`<saml2:EncryptedAssertion>`，`<saml2:EncryptedAttribute>`，和`<saml2:EncryptedID>`元素，通过使用解密[`Saml2X509Credential`实例]（overview.html# Servlet-saml2login-rpr-creditions）在[`RelyingPartyRegistration`]中注册的（overview.html# Servlet-saml2login-relyingpartyregistration）。

`OpenSaml4AuthenticationProvider`暴露[两种解密策略](overview.html#servlet-saml2login-architecture)。响应Decrypter用于解密`<saml2:Response>`中的加密元素，例如`<saml2:EncryptedAssertion>`。断言解密程序用于解密`<saml2:Assertion>`中的加密元素，例如`<saml2:EncryptedAttribute>`和`<saml2:EncryptedID>`。

可以替换`OpenSaml4AuthenticationProvider’s default decryption strategy with your own. For example, if you have a separate service that decrypts the assertions in a `<saml2:Response>`，可以这样使用：

Java

```
MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
```

Kotlin

```
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
```

如果你也在解密`<saml2:Assertion>`中的单个元素，那么也可以自定义断言解密程序：

Java

```
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
```

Kotlin

```
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
```

|   |由于断言可以与响应分开签名，因此有两个单独的解密器。<br/>在签名验证之前尝试解密签名断言的元素可能会使签名无效。<br/>如果你的断言方只签名响应，那么只使用响应解密器解密所有元素是安全的。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 使用自定义身份验证管理器

当然，`authenticationManager`DSL方法也可以用于执行完全自定义的SAML2.0身份验证。此身份验证管理器应该期望有一个`Saml2AuthenticationToken`对象，该对象包含SAML2.0响应XML数据。

Java

```
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
        http
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .authenticationManager(authenticationManager)
            )
        ;
    }
}
```

Kotlin

```
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
        http {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
    }
}
```

## 使用`Saml2AuthenticatedPrincipal`

在为给定的断言方正确配置了依赖方之后，它就可以接受断言了。一旦依赖方验证了一个断言，结果就是一个`Saml2Authentication`和一个`Saml2AuthenticatedPrincipal`。

这意味着你可以访问控制器中的主体，如下所示：

Java

```
@Controller
public class MainController {
	@GetMapping("/")
	public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
		String email = principal.getFirstAttribute("email");
		model.setAttribute("email", email);
		return "index";
	}
}
```

Kotlin

```
@Controller
class MainController {
    @GetMapping("/")
    fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
        val email = principal.getFirstAttribute<String>("email")
        model.setAttribute("email", email)
        return "index"
    }
}
```

|   |因为SAML2.0规范允许每个属性有多个值，所以你可以调用`getAttribute`来获取属性列表，也可以调用`getFirstAttribute`来获取列表中的第一个。当你知道只有一个值时，`getFirstAttribute`非常方便。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

[SAML2身份验证请求](authentication-requests.html)[SAML2注销](../logout.html)