SAML 2.0 Login Overview

我们首先检查 SAML 2.0 依靠方身份验证如何在 Spring Security 中起作用。首先,我们看到,像 RFC 1901 一样,Spring Security 会将用户带到第三方执行身份验证。它通过一系列重定向来执行此操作: .Redirecting to Asserting Party Authentication image::servlet/saml2/saml2webssoauthenticationrequestfilter.png[]

上图建立在我们的 SecurityFilterChainAbstractAuthenticationProcessingFilter图表之上:

number 1 首先,用户向 /private 资源发送未经验证的请求,并且对该资源没有获得授权。 number 2 Spring Security 的 AuthorizationFilter 通过抛出 AccessDeniedException 表明未经验证的请求是 Deniednumber 3由于用户缺乏授权,ExceptionTranslationFilter启动 Start Authentication。配置的 AuthenticationEntryPoint是 {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[LoginUrlAuthenticationEntryPoint] 的一个实例,它重定向到 the <saml2:AuthnRequest> generating endpointSaml2WebSsoAuthenticationRequestFilter。或者,如果您有 configured more than one asserting party,则它首先重定向到选择器页面。 number 4接下来,Saml2WebSsoAuthenticationRequestFilter`使用其配置的 <<`Saml2AuthenticationRequestFactory,servlet-saml2login-sp-initiated-factory>> 创建、签名、序列化和编码 <saml2:AuthnRequest>number 5然后浏览器将此 <saml2:AuthnRequest>`带给验证方。验证方尝试验证用户。如果成功,则将 `<saml2:Response>`返回给浏览器。 number 6然后浏览器将 `<saml2:Response> POST 到声明使用者服务端点。 下图显示了 Spring Security 如何对 RFC 1901 进行身份验证。

saml2webssoauthenticationfilter
Figure 1. Authenticating a <saml2:Response>

此图建立在我们的 SecurityFilterChain 图基础上。

number 1当浏览器向应用程序提交 <saml2:Response>`时,它会 delegates to `Saml2WebSsoAuthenticationFilter。此筛选器调用其配置的 AuthenticationConverter`以通过从 `HttpServletRequest`中提取响应来创建 `Saml2AuthenticationToken。此转换器另外解析 <<`RelyingPartyRegistration`,servlet-saml2login-relyingpartyregistration>> 并将其提供给 Saml2AuthenticationTokennumber 2接下来,筛选器将令牌传递给其配置的 AuthenticationManager。默认情况下,它使用 <<`OpenSamlAuthenticationProvider`,servlet-saml2login-architecture>>。 number 3如果验证失败,则 Failure

number 4如果验证成功,则 Success

Minimal Dependencies

SAML 2.0 服务提供程序支持驻留在 `spring-security-saml2-service-provider`中。它建立在 OpenSAML 库之上,因此,您还必须在构建配置中包含 Shibboleth Maven 存储库。查看 this link以了解有关为何需要单独的存储库的更多详细信息。

  • 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'
}

Minimal Configuration

在使用 Spring Boot时,将应用程序配置为服务提供程序包括两个基本步骤:包含所需的依赖项。指示必要的验证方元数据。

此外,此配置预设你已经 registered the relying party with your asserting party

Specifying Identity Provider Metadata

在 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

其中:

  • `https://idp.example.com/issuer`是在身份提供程序发布的SAML响应的`Issuer`属性中包含的值。

  • `classpath:idp.crt`是类路径上用于验证SAML响应的身份提供程序证书的位置。

  • `https://idp.example.com/issuer/sso`是身份提供程序预期`AuthnRequest`实例的端点。

  • `adfs`是an arbitrary identifier you choose

这就是全部!

身份提供程序和主张方是同义词,服务提供程序和依赖方也是同义词。这些通常分别缩写为 AP 和 RP。

Runtime Expectations

earlier 中进行配置中,应用程序处理包含 SAMLResponse 参数的任何 POST /login/saml2/sso/{registrationId} 请求:

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

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

有两种方法可以诱导主张方生成 SAMLResponse

  • 您可以导航到您的声明方。对于每个已注册的依赖方,它可能有一些链接或按钮,您可以单击它们来发送`SAMLResponse`。

  • 您可以导航到应用程序中的受保护页面,例如`http://localhost:8080`。然后,您的应用程序将重定向到配置的声明方,然后声明方将发送`SAMLResponse`。

从此处,考虑跳转到:

How SAML 2.0 Login Integrates with OpenSAML

Spring Security 的 SAML 2.0 支持有几个设计目标:

  • 依赖于SAML 2.0操作和域对象的库。为了实现这一点,Spring Security使用OpenSAML。

  • 确保在使用Spring Security的SAML支持时不需要此库。为了实现这一点,Spring Security在使用OpenSAML进行合同编制的任何接口或类都将保持封装。这使您可以将OpenSAML换成其他一些库或一个不受支持的OpenSAML版本。

作为这两个目标的自然结果,与其他模块相比,Spring Security 的 SAML API 非常小。相反,OpenSamlAuthenticationRequestFactoryOpenSamlAuthenticationProvider 这样的类会公开 Converter 实现,这些实现可以自定义身份验证过程中的各个步骤。

例如,一旦应用程序收到 SAMLResponse 并委托给 Saml2WebSsoAuthenticationFilter,过滤器将委托给 OpenSamlAuthenticationProvider

Authenticating an OpenSAML Response

opensamlauthenticationprovider

此图表基于 <<`Saml2WebSsoAuthenticationFilter` 图表,servlet-saml2login-authentication-saml2webssoauthenticationfilter>>。

number 1 Saml2WebSsoAuthenticationFilter`制定 `Saml2AuthenticationToken`并调用 `AuthenticationManager

number 2 AuthenticationManager调用 OpenSAML 身份验证提供程序。

number 3验证提供程序将响应反序列化为 OpenSAML `Response`并检查其签名。如果签名无效,则验证失败。

number 4然后提供程序 decrypts any EncryptedAssertion elements。如果任何解密失败,身份验证失败。

number 5接下来,提供者验证响应的 `Issuer`和 `Destination`值。如果它们与 `RelyingPartyRegistration`中的值不匹配,则验证失败。

number 6之后,提供者验证每个 `Assertion`的签名。如果任何签名无效,则验证失败。此外,如果响应或断言都没有签名,则验证失败。响应或所有断言都必须有签名。

number 7然后,提供程序 ,解密任何 `EncryptedID`或 `EncryptedAttribute`元素]。如果任何解密失败,身份验证失败。

number 8接下来,提供者验证每个断言的 ExpiresAt`和 `NotBefore`时间戳、<Subject>`和任何 `<AudienceRestriction>`条件。如果任何验证失败,则验证失败。

number 9在此之后,提供者获取第一个断言的 AttributeStatement`并将其映射到 `Map<String, List<Object>>。它还授予 `ROLE_USER`授予的权限。

number 10最后,它从第一个断言中获取 NameID,从属性中获取 Map,从 GrantedAuthority`构建了 `Saml2AuthenticatedPrincipal。然后,它将该主体和权限放入 `Saml2Authentication`中。

最终的 Authentication#getPrincipal 是一个 Spring Security Saml2AuthenticatedPrincipal 对象,而 Authentication#getName 映射到第一个断言的 NameID 元素。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId 保留 identifier to the associated RelyingPartyRegistration

Customizing OpenSAML Configuration

同时使用 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)
        }
    }
}

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

Overriding or Replacing Boot Auto Configuration

Spring Boot 会为依赖方生成两个 @Bean 对象。

第一个是 SecurityFilterChain,它把应用程序配置为依赖方。在包括 spring-security-saml2-service-provider 时,SecurityFilterChain 如下所示:

Default SAML 2.0 Login Configuration
  • 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()
}

如果应用程序没有公开 SecurityFilterChain bean,Spring Boot 会公开前面的默认 bean。

您可以通过在应用程序中公开 bean 来替换这个:

Custom SAML 2.0 Login Configuration
  • 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()
    }
}

前面的示例要求任何以 /messages/ 开头的 URL 都有 USER 的角色。

Spring Boot 创建的第二个 @Bean`为 {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[`RelyingPartyRegistrationRepository],它表示断言方和验证方元数据。这包括内容,例如,验证方在从断言方请求身份验证时应使用的 SSO 端点的位置。

您可以通过发布您自己的 RelyingPartyRegistrationRepository bean 来覆盖默认。例如,您可以通过访问断言方的元数据端点来查看其配置:

Relying Party Registration Repository
  • 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 是您选择的任意值,用于区分注册。

或者,您可以手动提供每个详细信息:

Relying Party Registration Repository Manual Configuration
  • 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)
}

X509Support 是一个 OpenSAML 类,在前面的代码段中为简洁而使用。

或者,您可以使用 DSL 直接连接存储库,该 DSL 也会覆盖自动配置的 SecurityFilterChain

Custom Relying Party Registration DSL
  • 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()
    }
}

依赖方可以通过在 RelyingPartyRegistrationRepository 中注册多于一个依赖方而成为多租户。

RelyingPartyRegistration

{security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[RelyingPartyRegistration] 实例表示验证方和断言方元数据之间的链接。

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()

注意,您还可以从任意 InputStream 源创建 RelyingPartyRegistration。一个这样的示例是当元数据存储在数据库中时:

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()

顶级元数据方法是关于依赖方的详细信息。assertingPartyDetails 中的方法是关于断言方的详细信息。

依赖方预期 SAML 响应的位置是断言使用者服务位置。

依赖方的 entityId 的默认值是 {baseUrl}/saml2/service-provider-metadata/{registrationId}。这是在配置断言方以了解您的依赖方所需的值。

assertionConsumerServiceLocation 的默认值为 /login/saml2/sso/{registrationId}。默认情况下,它映射到过滤器链中的 <<`Saml2WebSsoAuthenticationFilter`,servlet-saml2login-authentication-saml2webssoauthenticationfilter>>。

URI Patterns

您可能注意到了前面示例中的 {baseUrl}{registrationId} 占位符。

这些对于生成 URI 很有用。因此,依赖方的 entityIdassertionConsumerServiceLocation 支持以下占位符:

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

  • registrationId- 此依赖方的注册ID

  • baseScheme- 部署应用程序的方案

  • baseHost- 部署应用程序的主机

  • basePort- 部署应用程序的端口

例如,前面定义的 assertionConsumerServiceLocation 为:

/my-login-endpoint/{registrationId}

在已部署的应用程序中,它转换为:

/my-login-endpoint/adfs

前面显示的 entityId 被定义为:

{baseUrl}/{registrationId}

在已部署的应用程序中,这转换为:

当前的 URI 模式如下:

由于 registrationIdRelyingPartyRegistration 的主要标识符,因此在未经身份验证的情况下需要在 URL 中使用它。如果您出于任何原因希望从 URL 中删除 registrationId,您可以 specify a RelyingPartyRegistrationResolver 告诉 Spring Security 如何查找 registrationId

Credentials

earlier所显示的示例中,您还可能注意到使用凭证。

通常情况下,依赖方使用相同的密钥来签署有效负载以及解密它们。或者,它可以使用相同的密钥来验证有效负载以及加密它们。

因此,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 的 RsaKeyConverters 实用程序类加载第一个,并按照之前的做法加载第二个:

  • 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 会为您执行这些转换。

Duplicated Relying Party Configurations

当应用程序使用多个断言方时,某些配置会在 RelyingPartyRegistration 实例之间重复:

  • The relying party’s entityId

  • Its 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)
}

Resolving the RelyingPartyRegistration from the Request

如前所述,Spring Security 通过在 URI 路径中查找注册 ID 来解决 RelyingPartyRegistration

根据使用案例,也采用许多其他策略来推导策略。例如:

  • 用于处理`&lt;saml2:Response&gt;`s, the RelyingPartyRegistration`从相关的&lt;saml2:AuthRequest&gt;`或`&lt;saml2:Response#Issuer&gt;`元素中查找

  • 用于处理`&lt;saml2:LogoutRequest&gt;`s, the RelyingPartyRegistration`从当前登录的用户或&lt;saml2:LogoutRequest#Issuer&gt;`元素中查找

  • 用于发布元数据,RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable&lt;RelyingPartyRegistration&gt;

当需要进行调整时,您可以对针对自定义此功能的每个端点的特定组件进行调整:

  • 对于SAML响应,自定义`AuthenticationConverter`

  • 对于注销请求,自定义 Saml2LogoutRequestValidatorParametersResolver

  • 对于元数据,自定义 Saml2MetadataResponseResolver

Federating Login

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,您可以在上面样例中看到已完成该步骤,其中 entityIdassertionConsumerServiceLocation 配置有静态端点。

您可以在 我们的 `saml-extension-federation`示例 中看到它的一个完整示例。

Using Spring Security SAML Extension URIs

如果您正在从 Spring Security SAML Extension 迁移,可能通过将应用程序配置为使用 SAML Extension URI 默认值受益。

有关此内容的更多信息,请参阅 我们的 `custom-urls`示例我们的 `saml-extension-federation`示例