| 
         对于最新的稳定版本,请使用 Spring Security 6.4.1!  | 
    
SAML 2.0 登录概述
我们首先研究 SAML 2.0 依赖方身份验证在 Spring Security 中的工作原理。 首先,我们看到,与 OAuth 2.0 登录一样,Spring Security 将用户带到第三方进行身份验证。 它通过一系列重定向来实现这一点:
首先,用户向/private资源,但未获得授权。
Spring Security 的AuthorizationFilter表示未经身份验证的请求被拒绝,方法是抛出AccessDeniedException.
由于用户没有授权,因此ExceptionTranslationFilter启动 Start Authentication。
配置的AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint,它会重定向到这<saml2:AuthnRequest>生成终端节点,Saml2WebSsoAuthenticationRequestFilter.
或者,如果您配置了多个断言方,则它首先重定向到选取器页面。
接下来,Saml2WebSsoAuthenticationRequestFilter创建、签署、序列化和编码<saml2:AuthnRequest>使用其配置的Saml2AuthenticationRequestFactory.
然后浏览器会采用这个<saml2:AuthnRequest>并将其提交给主张方。
断言方尝试对用户进行身份验证。
如果成功,它将返回一个<saml2:Response>返回浏览器。
然后,浏览器将<saml2:Response>添加到断言使用者服务终端节点。
下图显示了 Spring Security 如何对<saml2:Response>.
<saml2:Response>| 
 该图建立在我们的  | 
当浏览器提交<saml2:Response>对于应用程序,它delegates 到Saml2WebSsoAuthenticationFilter.
此过滤器将其配置的AuthenticationConverter要创建一个Saml2AuthenticationToken通过从HttpServletRequest.
此转换器还解决了RelyingPartyRegistration并将其提供给Saml2AuthenticationToken.
接下来,过滤器将令牌传递给其配置的AuthenticationManager.
默认情况下,它使用OpenSamlAuthenticationProvider.
如果身份验证失败,则为 Failure。
- 
这
AuthenticationEntryPoint以重新启动身份验证过程。 
如果身份验证成功,则为 Success。
- 
这
Saml2WebSsoAuthenticationFilter调用FilterChain#doFilter(request,response)以继续执行 Application Logic 的其余部分。 
最小依赖项
SAML 2.0 服务提供商支持位于spring-security-saml2-service-provider.
它基于 OpenSAML 库构建,因此,您还必须在构建配置中包含 Shibboleth Maven 存储库。
查看此链接,了解有关为什么需要单独存储库的更多详细信息。
- 
Maven
 - 
Gradle
 
<repositories>
    <!-- ... -->
    <repository>
        <id>shibboleth-releases</id>
        <url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
    </repository>
</repositories>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
repositories {
    // ...
    maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
    // ...
    implementation 'org.springframework.security:spring-security-saml2-service-provider'
}
最小配置
使用 Spring Boot 时,将应用程序配置为服务提供商包括两个基本步骤: .包括所需的依赖项。 .指示必要的断言方元数据。
| 此外,此配置还假定您已向断言方注册了依赖方。 | 
指定身份提供程序元数据
在 Spring Boot 应用程序中,要指定身份提供者的元数据,请创建类似于以下内容的配置:
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
哪里:
- 
idp.example.com/issuer是Issuer身份提供商发出的 SAML 响应的属性。 - 
classpath:idp.crt是身份提供程序证书在类路径上的位置,用于验证 SAML 响应。 - 
idp.example.com/issuer/sso是身份提供商期望的终端节点AuthnRequest实例。 - 
adfs是您选择的任意标识符 
就是这样!
| 
 身份提供商和断言方是同义词,服务提供商和依赖方也是同义词。 它们通常分别缩写为 AP 和 RP。  | 
运行时预期
如前所述,应用程序会处理任何POST /login/saml2/sso/{registrationId}请求中包含SAMLResponse参数:
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
有两种方法可以诱使您的断言方生成SAMLResponse:
- 
您可以导航到您的断言方。 它可能为每个已注册的依赖方提供某种链接或按钮,您可以单击这些链接或按钮以将
SAMLResponse. - 
您可以导航到应用程序中的受保护页面,例如
localhost:8080. 然后,您的应用程序重定向到配置的断言方,然后该方将SAMLResponse. 
从这里,考虑跳到:
SAML 2.0 登录如何与 OpenSAML 集成
Spring Security 的 SAML 2.0 支持有几个设计目标:
- 
依赖 SAML 2.0作和域对象的库。 为了实现这一点,Spring Security 使用 OpenSAML。
 - 
确保在使用 Spring Security 的 SAML 支持时不需要此库。 为了实现这一点,Spring Security 在 Contract 中使用 OpenSAML 的任何接口或类都保持封装状态。 这样,您就可以将 OpenSAML 切换为其他库或不受支持的 OpenSAML 版本。
 
作为这两个目标的自然结果, Spring Security 的 SAML API 相对于其他模块来说非常小。
相反,诸如OpenSamlAuthenticationRequestFactory和OpenSamlAuthenticationProvider暴露Converter自定义身份验证过程中各个步骤的实现。
例如,一旦您的应用程序收到SAMLResponse和委托人Saml2WebSsoAuthenticationFilter,筛选器将委托给OpenSamlAuthenticationProvider:
Response
这Saml2WebSsoAuthenticationFilter制定Saml2AuthenticationToken并调用AuthenticationManager.
这AuthenticationManager调用 OpenSAML 身份验证提供程序。
身份验证提供程序将响应反序列化为 OpenSAMLResponse并检查其签名。
如果签名无效,则鉴权失败。
然后是提供商解密任何EncryptedAssertion元素.
如果任何解密失败,则身份验证将失败。
接下来,提供程序验证响应的Issuer和Destination值。
如果它们与RelyingPartyRegistration,则身份验证失败。
之后,提供程序会验证每个Assertion.
如果签名无效,则鉴权失败。
此外,如果响应和断言都没有签名,则身份验证将失败。
响应或所有断言都必须具有签名。
然后,提供程序 ,解密任何EncryptedID或EncryptedAttribute元素]。
如果任何解密失败,则身份验证将失败。
接下来,提供程序验证每个断言的ExpiresAt和NotBeforetimestamps、<Subject>和任何<AudienceRestriction>条件。
如果任何验证失败,则身份验证失败。
然后,提供程序采用第一个断言的AttributeStatement并将其映射到Map<String, List<Object>>.
它还授予ROLE_USER授予权限。
最后,它需要NameID从第一个断言开始,Mapof 属性和GrantedAuthority并构造一个Saml2AuthenticatedPrincipal.
然后,它将该 principal 和权限放入Saml2Authentication.
结果Authentication#getPrincipal是 Spring SecuritySaml2AuthenticatedPrincipalobject 和Authentication#getName映射到第一个断言的NameID元素。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId持有identifier 添加到关联的RelyingPartyRegistration.
自定义 OpenSAML 配置
任何同时使用 Spring Security 和 OpenSAML 的类都应该静态初始化OpenSamlInitializationService在课程开始时:
- 
Java
 - 
Kotlin
 
static {
	OpenSamlInitializationService.initialize();
}
companion object {
    init {
        OpenSamlInitializationService.initialize()
    }
}
这取代了 OpenSAML 的InitializationService#initialize.
有时,自定义 OpenSAML 构建、封送和取消封送 SAML 对象的方式可能很有价值。
在这些情况下,您可能希望调用OpenSamlInitializationService#requireInitialize(Consumer)这使您能够访问 OpenSAML 的XMLObjectProviderFactory.
例如,在发送未签名的 AuthNRequest 时,您可能希望强制重新进行身份验证。
在这种情况下,您可以注册自己的AuthnRequestMarshaller这样:
- 
Java
 - 
Kotlin
 
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);
    });
}
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)
        }
    }
}
这requireInitializemethod 每个应用程序实例只能调用一次。
覆盖或替换引导自动配置
Spring Boot 生成两个@Bean对象。
第一个是SecurityFilterChain,将应用程序配置为依赖方。
当包含spring-security-saml2-service-provider这SecurityFilterChain看来:
- 
Java
 - 
Kotlin
 
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults());
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        saml2Login { }
    }
    return http.build()
}
如果应用程序未公开SecurityFilterChainbean,则 Spring Boot 会公开前面的默认 Bean。
您可以通过在应用程序中公开 bean 来替换它:
- 
Java
 - 
Kotlin
 
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
            }
        }
        return http.build()
    }
}
前面的示例需要USER对于任何以/messages/.
第二个@BeanSpring Boot 创建的是一个RelyingPartyRegistrationRepository,它表示断言方和依赖方元数据。
这包括依赖方在向断言方请求身份验证时应使用的 SSO 端点的位置等内容。
您可以通过发布自己的RelyingPartyRegistrationRepository豆。
例如,您可以通过点击其元数据端点来查找断言方的配置:
- 
Java
 - 
Kotlin
 
@Value("${metadata.location}")
String assertingPartyMetadataLocation;
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation(assertingPartyMetadataLocation)
            .registrationId("example")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null
@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val registration = RelyingPartyRegistrations
        .fromMetadataLocation(assertingPartyMetadataLocation)
        .registrationId("example")
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
这registrationId是您选择用于区分注册的任意值。 | 
或者,您可以手动提供每个详细信息:
- 
Java
 - 
Kotlin
 
@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);
}
@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)
}
| 
 
  | 
或者,您可以使用 DSL 直接连接存储库,这也会覆盖自动配置的SecurityFilterChain:
- 
Java
 - 
Kotlin
 
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(saml2 -> saml2
                .relyingPartyRegistrationRepository(relyingPartyRegistrations())
            );
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                relyingPartyRegistrationRepository = relyingPartyRegistrations()
            }
        }
        return http.build()
    }
}
| 
 通过在  | 
RelyingParty注册
一个RelyingPartyRegistrationinstance 表示依赖方和断言方的元数据之间的链接。
在RelyingPartyRegistration,您可以提供依赖方元数据,例如其Issuer值,它希望将 SAML 响应发送到其中,以及它拥有的用于对负载进行签名或解密的任何凭证。
此外,您还可以提供断言参与方元数据,例如其Issuer值,它希望将 AuthnRequests 发送到其中,以及它拥有的任何公有凭证,以便依赖方验证或加密负载。
以下内容RelyingPartyRegistration是大多数设置所需的最低要求:
- 
Java
 - 
Kotlin
 
RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
        .fromMetadataLocation("https://ap.example.org/metadata")
        .registrationId("my-id")
        .build();
val relyingPartyRegistration = RelyingPartyRegistrations
    .fromMetadataLocation("https://ap.example.org/metadata")
    .registrationId("my-id")
    .build()
请注意,您还可以创建一个RelyingPartyRegistration从任意InputStream源。
一个这样的例子是当元数据存储在数据库中时:
String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadata(source)
            .registrationId("my-id")
            .build();
}
还可以进行更复杂的设置:
- 
Java
 - 
Kotlin
 
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();
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()
| 
 顶级元数据方法是有关信赖方的详细信息。
里面的方法  | 
| 
 依赖方需要 SAML 响应的位置是断言使用者服务位置。  | 
依赖方的entityId是{baseUrl}/saml2/service-provider-metadata/{registrationId}.
这是配置断言方以了解依赖方时所需的此值。
默认的assertionConsumerServiceLocation是/login/saml2/sso/{registrationId}.
默认情况下,它被映射到Saml2WebSsoAuthenticationFilter在过滤器链中。
URI 模式
您可能已经注意到了{baseUrl}和{registrationId}placeholders 的 Brackets 进行引用。
这些对于生成 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
流行的 URI 模式如下:
- 
/saml2/authenticate/{registrationId}- 端点生成一个<saml2:AuthnRequest>基于该RelyingPartyRegistration并将其发送给断言方 - 
/login/saml2/sso/- 端点验证断言方的<saml2:Response>;这RelyingPartyRegistration从先前的身份验证状态或响应的发布者(如果需要)中查找;还支持/login/saml2/sso/{registrationId} - 
/logout/saml2/sso- 端点过程<saml2:LogoutRequest>和<saml2:LogoutResponse>负载;这RelyingPartyRegistration从先前验证的状态或请求的颁发者(如果需要)中查找;还支持/logout/saml2/slo/{registrationId} - 
/saml2/metadata- 一组RelyingPartyRegistrations;还支持/saml2/metadata/{registrationId}或/saml2/service-provider-metadata/{registrationId}对于特定的RelyingPartyRegistration 
由于registrationId是RelyingPartyRegistration,则对于未经身份验证的方案,URL 中需要它。
如果要删除registrationId无论出于何种原因,您都可以指定一个RelyingPartyRegistrationResolver告诉 Spring Security 如何查找registrationId.
凭据
在前面显示的示例中,您可能还注意到了所使用的凭证。
通常,依赖方使用相同的密钥对有效负载进行签名和解密。 或者,它可以使用相同的密钥来验证有效负载并对其进行加密。
因此,Spring Security 附带了Saml2X509Credential,一种特定于 SAML 的凭证,可简化为不同使用案例配置相同密钥的过程。
您至少需要拥有来自断言方的证书,以便可以验证断言方的签名响应。
要构造Saml2X509Credential,您可以使用它来验证来自断言方的断言,您可以加载该文件并使用
这CertificateFactory:
- 
Java
 - 
Kotlin
 
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
    X509Certificate certificate = (X509Certificate)
            CertificateFactory.getInstance("X.509").generateCertificate(is);
    return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
    return Saml2X509Credential.verification(
        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
    )
}
假设断言方也要加密断言。 在这种情况下,信赖方需要私钥来解密加密的值。
在这种情况下,您需要一个RSAPrivateKey及其相应的X509Certificate.
您可以使用 Spring Security 的RsaKeyConvertersUtility 类和第二个类一样:
- 
Java
 - 
Kotlin
 
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);
}
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 Boot 属性时, Spring Boot 会为您执行这些转换。  | 
重复的依赖方配置
当应用程序使用多个断言方时,某些配置在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
 - 
Kotlin
 
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);
}
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)
}
解决RelyingPartyRegistration从请求
到目前为止,Spring Security 解析了RelyingPartyRegistration通过在 URI 路径中查找注册 ID。
根据用例,还采用了许多其他策略来派生一个。 例如:
- 
用于加工
<saml2:Response>`s, the `RelyingPartyRegistration从关联的<saml2:AuthRequest>或从<saml2:Response#Issuer>元素 - 
用于加工
<saml2:LogoutRequest>`s, the `RelyingPartyRegistration从当前登录的用户或<saml2:LogoutRequest#Issuer>元素 - 
对于发布元数据,
RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable<RelyingPartyRegistration> 
当这需要调整时,您可以转向每个端点的特定组件,以自定义此端点:
- 
对于 SAML 响应,自定义
AuthenticationConverter - 
对于 Logout Requests(注销请求),自定义
Saml2LogoutRequestValidatorParametersResolver - 
对于 Metadata (元数据),自定义
Saml2MetadataResponseResolver 
联合登录
SAML 2.0 的一种常见安排是具有多个断言方的身份提供商。
在这种情况下,身份提供商的元数据终端节点将返回多个<md:IDPSSODescriptor>元素。
这些多个断言方可以通过对RelyingPartyRegistrations这样:
- 
Java
 - 
Kotlin
 
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map((builder) -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .build()
        )
        .collect(Collectors.toList());
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map { builder : RelyingPartyRegistration.Builder -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
            .build()
        }
        .collect(Collectors.toList())
请注意,由于注册 ID 设置为随机值,因此这会将某些 SAML 2.0 端点更改为不可预测。 有几种方法可以解决这个问题;让我们专注于一种适合联合特定用例的方法。
在许多联合身份验证情况下,所有断言方共享服务提供商配置。
鉴于 Spring Security 默认包含registrationId在服务提供商元数据中,另一个步骤是更改相应的 URI 以排除registrationId,您可以看到,在上面的示例中已经完成了此作,其中entityId和assertionConsumerServiceLocation配置了静态终端节点。
使用 Spring Security SAML 扩展 URI
如果要从 Spring Security SAML 扩展迁移,则将应用程序配置为使用 SAML 扩展 URI 默认值可能会有一些好处。
有关更多信息,请参阅我们custom-urls样本和我们saml-extension-federation样本.