# SAML2.0登录概述

让我们来看看SAML2.0依赖方身份验证在 Spring 安全性中是如何工作的。首先，我们看到，与[OAuth2.0登录](../../oauth2/login/index.html)类似， Spring 安全性将用户带到第三方进行身份验证。它通过一系列重定向来实现这一点。

![SAML2WebssoAuthenticationRequestFilter](https://docs.spring.io/spring-security/reference/_images/servlet/saml2/saml2webssoauthenticationrequestfilter.png)

图1.重定向到断言的一方身份验证

上图构建了我们的[`SecurityFilterChain`]（../../architecture.html# Servlet-securityfilterchain）和[`AbstractAuthenticationProcessingFilter`]（.../../authentication/architecture.html# Servlet-authentication-abstractprocessingfilter）图：

![number 1](https://docs.spring.io/spring-security/reference/_images/icons/number_1.png)首先，用户向资源`/private`发出未经授权的请求。

![number 2](https://docs.spring.io/spring-security/reference/_images/icons/number_2.png) Spring security的[`FilterSecurityInterceptor`]（.../授权/authorization/authorization/authorization-requests.html# Servlet-authorization-filtersecurityinterceptor）通过抛出`AccessDeniedException`表示未经验证的请求*拒绝*。

![number 3](https://docs.spring.io/spring-security/reference/_images/icons/number_3.png)由于用户缺乏授权，[`ExceptionTranslationFilter`]（../../architecture.html# Servlet-ExceptionTranslationFilter）发起*启动身份验证*。配置的[`AuthenticationEntryPoint`]（../.../authentication/architecture.html# Servlet-authentication-authenticationentryPoint）是[`LoginUrlAuthenticationEntryPoint`]（https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/authentication/loginurlauthenticationentrypoint.html）的一个实例，它会重定向到[`<saml2:AuthnRequest>`-genering-endpoint]（authenticationcentification-exposit或者，如果你已经[配置了多个断言对象](#servlet-saml2login-relyingpartyregistrationrepository)，它将首先重定向到一个选择器页面。

![number 4](https://docs.spring.io/spring-security/reference/_images/icons/number_4.png)下一步，`Saml2WebSsoAuthenticationRequestFilter`使用其配置的[`Saml2AuthenticationRequestFactory`]（# Servlet-saml2login-sp-initiated-factory）创建、签名、序列化和编码`<saml2:AuthnRequest>`。

![number 5](https://docs.spring.io/spring-security/reference/_images/icons/number_5.png)然后，浏览器接收这个`<saml2:AuthnRequest>`并将其呈现给断言的一方。断言一方试图对用户进行身份验证。如果成功，它将返回一个`<saml2:Response>`返回到浏览器。

![number 6](https://docs.spring.io/spring-security/reference/_images/icons/number_6.png)浏览器然后将`<saml2:Response>`发布到断言消费者服务端点。

![SAML2WebssoAuthenticationFilter](https://docs.spring.io/spring-security/reference/_images/servlet/saml2/saml2webssoauthenticationfilter.png)

图2.验证`<saml2:Response>`

这个图是基于我们的[`SecurityFilterChain`]（../../architecture.html# Servlet-SecurityFilterchain）图构建的。

![number 1](https://docs.spring.io/spring-security/reference/_images/icons/number_1.png)当浏览器向应用程序提交`<saml2:Response>`时，它[委托给`Saml2WebSsoAuthenticationFilter`]（authentication.html# Servlet-saml2login-attenticate-responses）。该过滤器调用其配置的`AuthenticationConverter`，通过从`HttpServletRequest`中提取响应来创建`Saml2AuthenticationToken`。该转换器还解析[`重新注册政党`]（# Servlet-saml2login-relyingpartyregistration）并将其提供给`Saml2AuthenticationToken`。

![number 2](https://docs.spring.io/spring-security/reference/_images/icons/number_2.png)接下来，筛选器将令牌传递给其配置的[`AuthenticationManager`]（.../authentication/architecture.html# Servlet-authentication-providermanager）。默认情况下，它将使用[`OpenSAML authentication provider`]（# Servlet-saml2login-architecture）。

![number 3](https://docs.spring.io/spring-security/reference/_images/icons/number_3.png)如果身份验证失败，则*失败*

* [`SecurityContextHolder`]（../../authentication/architecture.html# Servlet-authentication-securitycontextholder）被清除。

* 调用[`AuthenticationEntryPoint`]（../../authentication/architecture.html# Servlet-authentication-authenticationentryPoint）来重新启动身份验证过程。

![number 4](https://docs.spring.io/spring-security/reference/_images/icons/number_4.png)如果身份验证成功，则*成功*。

* [`Authentication`]（../../authentication/architecture.html# Servlet-authentication-authentication）设置在[`SecurityContextHolder`]（../.../authentication/architecture.html# Servlet-authentication-securitycontexolder）上。

* `Saml2WebSsoAuthenticationFilter`调用`FilterChain#doFilter(request,response)`以继续应用程序逻辑的其余部分。

## 最小依赖

SAML2.0服务提供者支持驻留在`spring-security-saml2-service-provider`中。它是在OpenSAML库的基础上构建的。

## 最小配置

当使用[Spring Boot](https://spring.io/projects/spring-boot)时，将应用程序配置为服务提供者包括两个基本步骤。首先，包括所需的依赖关系，其次，指示必要的断言方元数据。

|   |另外，这也意味着你已经[向你的主张方登记了依赖方。](../metadata.html#servlet-saml2login-metadata)了。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------|

### 指定身份提供程序元数据

在 Spring 引导应用程序中，要指定身份提供者的元数据，只需执行以下操作：

```
spring:
  security:
    saml2:
      relyingparty:
        registration:
          adfs:
            identityprovider:
              entity-id: https://idp.example.com/issuer
              verification.credentials:
                - certificate-location: "classpath:idp.crt"
              singlesignon.url: https://idp.example.com/issuer/sso
              singlesignon.sign-request: false
```

哪里

* `[https://idp.example.com/issuer](https://idp.example.com/issuer)`是身份提供程序将发出的SAML响应的`Issuer`属性中包含的值

* `classpath:idp.crt`是 Classpath 上用于验证SAML响应的身份提供程序证书的位置，并且

* `[https://idp.example.com/issuer/sso](https://idp.example.com/issuer/sso)`是标识提供程序期望`AuthnRequest`s的端点。

* `adfs`是[你选择的任意标识符](#servlet-saml2login-relyingpartyregistrationid)

就这样！

|   |标识提供者和声明方是同义词，服务提供者和依赖方也是同义词。<br/>它们通常分别缩写为AP和RP。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 运行时期望

按照上面的配置，应用程序处理任何`POST /login/saml2/sso/{registrationId}`请求，其中包含`SAMLResponse`参数：

```
POST /login/saml2/sso/adfs HTTP/1.1

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
```

有两种方法可以看到诱导你的断言一方生成`SAMLResponse`:

* 首先，你可以导航到你的主张的一方。它可能为每个注册的依赖方提供了某种类型的链接或按钮，你可以单击这些链接或按钮发送`SAMLResponse`。

* 其次，你可以导航到应用程序中的受保护页面，例如`[http://localhost:8080](http://localhost:8080)`。然后，你的应用程序将重定向到配置的断言对象，然后发送`SAMLResponse`。

从这里开始，考虑跳到：

* [SAML2.0Login如何与OpenSAML集成](#servlet-saml2login-architecture)

* [如何使用`Saml2AuthenticatedPrincipal`]（authentication.html# Servlet-saml2login-authenticatedprincipal）

* [How to Override or Replace Spring Boot’s Auto Configuration](#servlet-saml2login-sansboot)

## SAML2.0Login如何与OpenSAML集成

Spring Security的SAML2.0支持有两个设计目标：

* 首先，依赖SAML2.0操作和域对象的库。为了实现这一点， Spring Security使用了OpenSAML。

* 其次，确保在使用 Spring Security的SAML支持时不需要这个库。为了实现这一点， Spring Security在契约中使用OpenSAML的任何接口或类都保持封装。这使得你可以将OpenSAML转换为其他一些库，甚至是OpenSAML的不支持版本。

作为上述两个目标的自然结果， Spring Security的SAML API相对于其他模块来说非常小。相反，像`OpenSaml4AuthenticationRequestFactory`和`OpenSaml4AuthenticationProvider`这样的类暴露了在身份验证过程中定制各种步骤的`Converter`s。

例如，一旦应用程序接收到`SAMLResponse`并委托给`Saml2WebSsoAuthenticationFilter`，过滤器将委托给`OpenSaml4AuthenticationProvider`。

|   |对于向后兼容性， Spring Security默认情况下将使用最新的OpenSAML3.，注意，尽管OpenSAML3已经达到了使用寿命，建议更新到OpenSAML4.x。，由于这个原因， Spring Security同时支持OpenSAML3.x和4.x。，如果你将OpenSAML依赖关系管理到4.x，<gt R=“110”/，然后 Spring Security将选择其OpenSAML4.x实现。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

验证OpenSAML`Response`

![OpenSAMLauthenticationProvider](https://docs.spring.io/spring-security/reference/_images/servlet/saml2/opensamlauthenticationprovider.png)

此图构建于[`Saml2WebSsoAuthenticationFilter`图]（# Servlet-SAML2login-Authentication-SAML2WebssoAuthenticationFilter）。

![number 1](https://docs.spring.io/spring-security/reference/_images/icons/number_1.png)the`Saml2WebSsoAuthenticationFilter`表示`Saml2AuthenticationToken`并调用[`AuthenticationManager`]（.././authentication/architecture.html# Servlet-authentication-providermanager）。

![number 2](https://docs.spring.io/spring-security/reference/_images/icons/number_2.png)[`AuthenticationManager`]（../../authentication/architecture.html# Servlet-authentication-providermanager）调用OpenSAML身份验证提供者。

![number 3](https://docs.spring.io/spring-security/reference/_images/icons/number_3.png)身份验证提供程序将响应反序列化为OpenSAML`Response`并检查其签名。如果签名无效，则验证失败。

![number 4](https://docs.spring.io/spring-security/reference/_images/icons/number_4.png)然后，提供程序[解密任何`EncryptedAssertion`元素]（authentication.html# Servlet-saml2login-opensamlauthenticationprovider-decryption）。如果任何解密失败，验证就失败了。

![number 5](https://docs.spring.io/spring-security/reference/_images/icons/number_5.png)接下来，提供程序验证响应的`Issuer`和`Destination`值。如果它们与`RelyingPartyRegistration`中的内容不匹配，则验证失败。

![number 6](https://docs.spring.io/spring-security/reference/_images/icons/number_6.png)之后，提供者验证每个`Assertion`的签名。如果任何签名无效，则验证失败。此外，如果响应和断言都没有签名，则身份验证失败。响应或所有断言都必须具有签名。

![number 7](https://docs.spring.io/spring-security/reference/_images/icons/number_7.png)然后，提供程序[,](authentication.html#servlet-saml2login-opensamlauthenticationprovider-decryption)解密任何`EncryptedID`或`EncryptedAttribute`元素]。如果任何解密失败，验证就失败了。

![number 8](https://docs.spring.io/spring-security/reference/_images/icons/number_8.png)接下来，提供程序验证每个断言的`ExpiresAt`和`NotBefore`时间戳、`<Subject>`和任何`<AudienceRestriction>`条件。如果任何验证失败，则验证失败。

![number 9](https://docs.spring.io/spring-security/reference/_images/icons/number_9.png)之后，提供者获取第一个断言的`AttributeStatement`，并将其映射到`Map<String, List<Object>>`。它还授予`ROLE_USER`授权。

![number 10](https://docs.spring.io/spring-security/reference/_images/icons/number_10.png)最后，它从第一个断言、属性的`Map`和`GrantedAuthority`中获取`NameID`，并构造`Saml2AuthenticatedPrincipal`。然后，它把委托人和当权者归为`Saml2Authentication`。

得到的`Authentication#getPrincipal`是一个 Spring security`Saml2AuthenticatedPrincipal`对象，并且`Authentication#getName`映射到第一个断言的`NameID`元素。`Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId`保存了关联`RelyingPartyRegistration`的[标识符]（# Servlet-saml2login-relyingpartyregistrationID）。

### 定制OpenSAML配置

同时使用 Spring Security和OpenSAML的任何类都应该在类的开头静态初始化`OpenSamlInitializationService`，如下所示：

Java

```
static {
	OpenSamlInitializationService.initialize();
}
```

Kotlin

```
companion object {
    init {
        OpenSamlInitializationService.initialize()
    }
}
```

这取代了OpenSAML的`InitializationService#initialize`。

有时，定制OpenSAML构建、Marshalls和unmarshallsSAML对象的方式是很有价值的。在这种情况下，你可能希望调用`OpenSamlInitializationService#requireInitialize(Consumer)`，这样就可以访问OpenSAML的`XMLObjectProviderFactory`。

例如，当发送未签名的authnrequest时，你可能希望强制进行重新身份验证。在这种情况下，你可以注册自己的`AuthnRequestMarshaller`，如下所示：

Java

```
static {
    OpenSamlInitializationService.requireInitialize(factory -> {
        AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
            @Override
            public Element marshall(XMLObject object, Element element) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, element);
            }

            public Element marshall(XMLObject object, Document document) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, document);
            }

            private void configureAuthnRequest(AuthnRequest authnRequest) {
                authnRequest.setForceAuthn(true);
            }
        }

        factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
    });
}
```

Kotlin

```
companion object {
    init {
        OpenSamlInitializationService.requireInitialize {
            val marshaller = object : AuthnRequestMarshaller() {
                override fun marshall(xmlObject: XMLObject, element: Element): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, element)
                }

                override fun marshall(xmlObject: XMLObject, document: Document): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, document)
                }

                private fun configureAuthnRequest(authnRequest: AuthnRequest) {
                    authnRequest.isForceAuthn = true
                }
            }
            it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
        }
    }
}
```

每个应用程序实例只能调用`requireInitialize`方法一次。

## 覆盖或替换Boot Auto配置

Spring Boot为依赖方生成两个`@Bean`s。

第一种是将应用程序配置为依赖方的`WebSecurityConfigurerAdapter`。当包含`spring-security-saml2-service-provider`时，`WebSecurityConfigurerAdapter`看起来像：

例1.默认的JWT配置

Java

```
protected void configure(HttpSecurity http) {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults());
}
```

Kotlin

```
fun configure(http: HttpSecurity) {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        saml2Login { }
    }
}
```

如果应用程序不公开`WebSecurityConfigurerAdapter` Bean，那么 Spring 引导将公开上面的默认引导。

你可以通过在应用程序中公开 Bean 来替换此选项：

例2.自定义SAML2.0登录配置

Java

```
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
    }
}
```

Kotlin

```
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
            }
        }
    }
}
```

对于任何以`/messages/`开头的URL，上面要求`USER`的角色。

第二个`@Bean` Spring 引导创建的是一个[`RelyingPartyRegistrationRepository`]（https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/saml2/provider/service/registration/registrationpartyregistrationrepository.html），它代表了主张方和依赖方的元数据。这包括诸如依赖方在请求断言方验证时应使用的SSO端点的位置之类的内容。

你可以通过发布自己的`RelyingPartyRegistrationRepository` Bean 来覆盖默认值。例如，你可以通过点击其元数据端点来查找断言一方的配置，如下所示：

例3.依赖方注册存储库

Java

```
@Value("${metadata.location}")
String assertingPartyMetadataLocation;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation(assertingPartyMetadataLocation)
            .registrationId("example")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
```

Kotlin

```
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val registration = RelyingPartyRegistrations
        .fromMetadataLocation(assertingPartyMetadataLocation)
        .registrationId("example")
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
```

|   |`registrationId`是你选择的用于区分注册的任意值。|
|---|-----------------------------------------------------------------------------------------------------|

或者，你可以手动提供每个细节，如下所示：

例4.依赖方注册存储库手动配置

Java

```
@Value("${verification.key}")
File verificationKey;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
    X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
    Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
    RelyingPartyRegistration registration = RelyingPartyRegistration
            .withRegistrationId("example")
            .assertingPartyDetails(party -> party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials(c -> c.add(credential))
            )
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
```

Kotlin

```
@Value("\${verification.key}")
var verificationKey: File? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
    val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
    val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
    val registration = RelyingPartyRegistration
        .withRegistrationId("example")
        .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
            party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
                    c.add(
                        credential
                    )
                }
        }
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
```

|   |请注意，`X509Support`是一个OpenSAML类，在这里的代码片段中使用它是为了简洁|
|---|----------------------------------------------------------------------------------|

或者，你可以使用DSL直接连接存储库，这也将覆盖自动配置的`WebSecurityConfigurerAdapter`:

例5.自定义依赖方注册DSL

Java

```
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeHttpRequests(authorize -> authorize
                .mvcMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(relyingPartyRegistrations())
            );
    }
}
```

Kotlin

```
@EnableWebSecurity
class MyCustomSecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                relyingPartyRegistrationRepository = relyingPartyRegistrations()
            }
        }
    }
}
```

|   |通过在`RelyingPartyRegistrationRepository`中注册多个依赖方，依赖方可以是多租户。|
|---|---------------------------------------------------------------------------------------------------------------------------|

## RelyingPartyRegistration

[`RelyingPartyRegistration`]（https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/saml2/provider/service/registration/registration/relypartyregistration.html）实例表示依赖方和断言方元数据之间的链接。

在`RelyingPartyRegistration`中，你可以提供依赖方的元数据，比如它的`Issuer`值，它希望将SAML响应发送到该值，以及它为签名或解密有效负载而OWNS的任何凭据。

此外，你还可以提供断言方元数据，比如它的`Issuer`值（它期望向其发送authnrequests），以及它为依赖方验证或加密有效负载而OWNS的任何公共凭据。

以下`RelyingPartyRegistration`是大多数设置所需的最低要求：

Java

```
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
        .fromMetadataLocation("https://ap.example.org/metadata")
        .registrationId("my-id")
        .build();
```

Kotlin

```
val relyingPartyRegistration = RelyingPartyRegistrations
    .fromMetadataLocation("https://ap.example.org/metadata")
    .registrationId("my-id")
    .build()
```

请注意，你也可以从任意的`InputStream`源创建`RelyingPartyRegistration`。一个这样的例子是当元数据存储在数据库中时：

```
String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadata(source)
            .registrationId("my-id")
            .build();
}
```

尽管更复杂的设置也是可能的，比如：

Java

```
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails(party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        )
        .build();
```

Kotlin

```
val relyingPartyRegistration =
    RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
            c.add(relyingPartyDecryptingCredential())
        }
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()
```

|   |顶级元数据方法是关于依赖方的详细信息。<br/>`assertingPartyDetails`中的方法是关于断言方的详细信息。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------|

|   |依赖方期望SAML响应的位置是断言消费者服务位置。|
|---|----------------------------------------------------------------------------------------------------------|

依赖方的`entityId`的默认值是`{baseUrl}/saml2/service-provider-metadata/{registrationId}`。这是在配置主张方以了解你的依赖方时所需的值。

`assertionConsumerServiceLocation`的默认值是`/login/saml2/sso/{registrationId}`。默认情况下，它被映射到过滤器链中的[`Saml2WebSsoAuthenticationFilter`]（# Servlet-SAML2login-Authentication-SAML2WebssoAuthenticationFilter）。

### URI模式

你可能在上面的示例中注意到了`{baseUrl}`和`{registrationId}`占位符。

这些对于生成URI非常有用。因此，依赖方的`entityId`和`assertionConsumerServiceLocation`支持以下占位符：

* `baseUrl`-已部署应用程序的方案、主机和端口

* `registrationId`-此依赖方的注册ID

* `baseScheme`-已部署应用程序的方案

* `baseHost`-已部署应用程序的主机

* `basePort`-已部署应用程序的端口

例如，上面定义的`assertionConsumerServiceLocation`是：

`/my-login-endpoint/{registrationId}`

在已部署的应用程序中，它将转换为

`/my-login-endpoint/adfs`

上述`entityId`定义为：

`{baseUrl}/{registrationId}`

在已部署的应用程序中，它将转换为

`https://rp.example.com/adfs`

### 证书

你还可能注意到了所使用的凭据。

通常情况下，依赖方将使用相同的密钥对有效载荷进行签名和解密。或者，它将使用相同的密钥来验证有效负载并对其进行加密。

因此， Spring Security附带`Saml2X509Credential`，这是一种特定于SAML的凭据，它简化了针对不同用例配置相同密钥的过程。

至少，有必要获得申述方的证明，这样申述方的签名回复才能得到验证。

要构造一个`Saml2X509Credential`来验证来自断言一方的断言，你可以加载该文件并使用`CertificateFactory`，如下所示：

Java

```
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
    X509Certificate certificate = (X509Certificate)
            CertificateFactory.getInstance("X.509").generateCertificate(is);
    return Saml2X509Credential.verification(certificate);
}
```

Kotlin

```
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
    return Saml2X509Credential.verification(
        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
    )
}
```

让我们假设，主张权利的一方也将对主张进行加密。在这种情况下，依赖方将需要一个私钥来解密加密值。

在这种情况下，你将需要一个`RSAPrivateKey`及其对应的`X509Certificate`。你可以使用 Spring Security的`RsaKeyConverters`实用程序类加载第一个，然后像以前一样加载第二个：

Java

```
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
    RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
    return Saml2X509Credential.decryption(rsa, certificate);
}
```

Kotlin

```
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
    val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
    return Saml2X509Credential.decryption(rsa, certificate)
}
```

|   |当你将这些文件的位置指定为适当的 Spring 引导属性时， Spring 引导将为你执行这些转换。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------|

### 从请求中解决依赖方

到目前为止， Spring Security通过在URI路径中查找注册ID来解析`RelyingPartyRegistration`。

你可能想要定制的原因有很多。其中：

* 你可能知道你永远不会是一个多租户应用程序，因此希望有一个更简单的URL方案

* 你可以通过URI路径以外的方式来识别租户。

要定制解析`RelyingPartyRegistration`的方式，可以配置自定义的`RelyingPartyRegistrationResolver`。默认值从URI的Last PATH元素中查找注册ID，并在`RelyingPartyRegistrationRepository`中查找它。

你可以提供一个更简单的解析器，例如，它总是返回相同的依赖方：

Java

```
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {

    private final RelyingPartyRegistrationResolver delegate;

    public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
        this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
    }

    @Override
    public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
        return this.delegate.resolve(request, "single");
    }
}
```

Kotlin

```
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
    override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
        return this.delegate.resolve(request, "single")
    }
}
```

然后，你可以将此解析器提供给适当的过滤器，这些过滤器可以[产生`<saml2:AuthnRequest>`s]（authentication-requests.html# Servlet-saml2login-sp-initiated-factory），[authenticate`<saml2:Response>`s]（authentication.html# Servlet-saml2login-authenticate-responses-responses），和[产生`<saml2:SPSSODescriptor>`元数据]（../metadata.html# Servlet-saml2

|   |请记住，如果你的`RelyingPartyRegistration`中有任何占位符，那么你的解析器实现应该解析它们。|
|---|--------------------------------------------------------------------------------------------------------------------------------|

### 重复的依赖方配置

当应用程序使用多个断言对象时，在`RelyingPartyRegistration`实例之间会重复一些配置：

* 依赖方的`entityId`

* 其`assertionConsumerServiceLocation`，以及

* 它的凭据，例如它的签名或解密凭据

这种设置的好处是，对于某些身份提供者来说，证书的轮换可能比其他身份提供者更容易。

复制可以通过几种不同的方式来减轻。

首先，在YAML中，可以通过引用来缓解这种情况，例如：

```
spring:
  security:
    saml2:
      relyingparty:
        okta:
          signing.credentials: &relying-party-credentials
            - private-key-location: classpath:rp.key
              certificate-location: classpath:rp.crt
          identityprovider:
            entity-id: ...
        azure:
          signing.credentials: *relying-party-credentials
          identityprovider:
            entity-id: ...
```

其次，在数据库中，不需要复制`RelyingPartyRegistration`的模型。

第三，在 Java 中，你可以创建一个自定义的配置方法，例如：

Java

```
private RelyingPartyRegistration.Builder
        addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {

    Saml2X509Credential signingCredential = ...
    builder.signingX509Credentials(c -> c.addAll(signingCredential));
    // ... other relying party configurations
}

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration okta = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("okta")).build();

    RelyingPartyRegistration azure = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("azure")).build();

    return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
```

Kotlin

```
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
    val signingCredential: Saml2X509Credential = ...
    builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
        c.add(
            signingCredential
        )
    }
    // ... other relying party configurations
}

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val okta = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("okta")
    ).build()
    val azure = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("azure")
    ).build()
    return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}
```

[SAML2登录](index.html)[SAML2身份验证请求](authentication-requests.html)