SAML 2.0 登录概述

我们首先研究一下 SAML 2.0 依赖方身份验证如何在 Spring Security 中工作。首先,我们会看到,与OAuth 2.0 登录一样,Spring Security 会将用户带到第三方进行身份验证。这是通过一系列重定向实现的。

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

number 1 首先,用户向其未被授权的/private资源发出未经身份验证的请求。

number 2 Spring Security 的AuthorizationFilter通过抛出AccessDeniedException指示未经身份验证的请求被拒绝

number 3 由于用户缺乏授权,ExceptionTranslationFilter启动身份验证。配置的AuthenticationEntryPointLoginUrlAuthenticationEntryPoint的实例,它重定向到生成<saml2:AuthnRequest>的端点Saml2WebSsoAuthenticationRequestFilter。或者,如果您已配置了多个断言方,它会首先重定向到一个选择页面。

number 4接下来,Saml2WebSsoAuthenticationRequestFilter使用其配置的Saml2AuthenticationRequestFactory创建、签名、序列化和编码<saml2:AuthnRequest>

number 5然后,浏览器获取此<saml2:AuthnRequest>并将其呈现给断言方。断言方尝试对用户进行身份验证。如果成功,它会将<saml2:Response>返回给浏览器。

number 6然后,浏览器将<saml2:Response> POST 到断言使用者服务端点。

下图显示了 Spring Security 如何验证<saml2:Response>

saml2webssoauthenticationfilter
图 2. 验证<saml2:Response>

此图基于我们的SecurityFilterChain图。

number 1当浏览器向应用程序提交<saml2:Response>时,它委托给Saml2WebSsoAuthenticationFilter。此过滤器调用其配置的AuthenticationConverter,通过从HttpServletRequest中提取响应来创建Saml2AuthenticationToken。此转换器还会解析RelyingPartyRegistration并将其提供给Saml2AuthenticationToken

number 2接下来,过滤器将其配置的AuthenticationManager传递令牌。默认情况下,它使用OpenSamlAuthenticationProvider

number 3如果身份验证失败,则为失败

number 4如果身份验证成功,则为成功

  • Authentication设置在SecurityContextHolder上。

  • Saml2WebSsoAuthenticationFilter 调用 FilterChain#doFilter(request,response) 来继续执行应用程序的其余逻辑。

最小依赖项

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

其中

就是这样!

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

运行时期望

如前面所配置,应用程序处理任何包含 SAMLResponse 参数的 POST /login/saml2/sso/{registrationId} 请求

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 在契约中使用 OpenSAML 的任何接口或类都保持封装。这使得您可以将 OpenSAML 替换为其他库或不受支持的 OpenSAML 版本。

作为这两个目标的自然结果,Spring Security 的 SAML API 相对于其他模块来说非常小。相反,诸如 OpenSamlAuthenticationRequestFactoryOpenSamlAuthenticationProvider 之类的类公开了 Converter 实现,这些实现自定义了身份验证过程中的各个步骤。

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

验证 OpenSAML Response

opensamlauthenticationprovider

number 1 Saml2WebSsoAuthenticationFilter 制定 Saml2AuthenticationToken 并调用AuthenticationManager

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

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

number 4 然后,提供程序解密任何 EncryptedAssertion 元素。如果任何解密失败,则身份验证失败。

number 5接下来,提供程序验证响应的 IssuerDestination 值。如果它们与 RelyingPartyRegistration 中的值不匹配,则身份验证失败。

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

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

number 8接下来,提供程序验证每个断言的 ExpiresAtNotBefore 时间戳、<Subject> 和任何 <AudienceRestriction> 条件。如果任何验证失败,则身份验证失败。

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

number 10最后,它获取第一个断言的 NameID、属性的 MapGrantedAuthority 并构建 Saml2AuthenticatedPrincipal。然后,它将该主体和权限放入 Saml2Authentication 中。

生成的 Authentication#getPrincipal 是 Spring Security Saml2AuthenticatedPrincipal 对象,Authentication#getName 映射到第一个断言的 NameID 元素。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId 包含与关联的 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)
        }
    }
}

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

覆盖或替换 Boot 自动配置

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

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

默认 SAML 2.0 登录配置
  • 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 来替换它

自定义 SAML 2.0 登录配置
  • 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 创建的第二个 @BeanRelyingPartyRegistrationRepository,它表示断言方和依赖方元数据。这包括依赖方在向断言方请求身份验证时应使用的 SSO 端点的位置等内容。

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

依赖方注册库
  • 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)
}

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

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

自定义依赖方注册 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

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

URI模式

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

这些占位符用于生成URI。因此,受信方(relying party)的entityIdassertionConsumerServiceLocation支持以下占位符:

  • 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} - 基于该RelyingPartyRegistration的配置生成<saml2:AuthnRequest> 并将其发送给断言方的端点

  • /login/saml2/sso/ - 验证断言方<saml2:Response>的端点;根据先前身份验证的状态或响应的发行者(如有需要)查找RelyingPartyRegistration;也支持/login/saml2/sso/{registrationId}

  • /logout/saml2/sso - 处理<saml2:LogoutRequest><saml2:LogoutResponse>有效负载的端点;根据先前身份验证的状态或请求的发行者(如有需要)查找RelyingPartyRegistration;也支持/logout/saml2/slo/{registrationId}

  • /saml2/metadata - 一组RelyingPartyRegistration的受信方元数据;也支持/saml2/metadata/{registrationId}/saml2/service-provider-metadata/{registrationId} 用于特定的RelyingPartyRegistration

由于registrationIdRelyingPartyRegistration的主要标识符,因此在未经身份验证的情况下,URL 中需要它。如果出于任何原因希望从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 的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 会为您执行这些转换。

重复的受信方配置

当应用程序使用多个断言方时,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 通过在URI路径中查找注册ID来解析RelyingPartyRegistration

根据用例的不同,还采用许多其他策略来推导出一个。例如:

  • 对于处理<saml2:Response>,从关联的<saml2:AuthRequest><saml2:Response#Issuer>元素中查找RelyingPartyRegistration

  • 对于处理<saml2:LogoutRequest>,从当前登录的用户或<saml2:LogoutRequest#Issuer>元素中查找RelyingPartyRegistration

  • 对于发布元数据,从也实现Iterable<RelyingPartyRegistration>的任何存储库中查找RelyingPartyRegistration

当需要调整此设置时,您可以转向针对自定义此设置的每个端点的特定组件:

  • 对于 SAML 响应,自定义AuthenticationConverter

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

  • 对于元数据,自定义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,您可以在上面的示例中看到这一点,其中entityIdassertionConsumerServiceLocation配置了静态端点。

您可以在我们的saml-extension-federation示例中看到此示例的完整代码。

使用 Spring Security SAML 扩展 URI

如果您要从 Spring Security SAML 扩展迁移,则配置应用程序以使用 SAML 扩展 URI 默认值可能有一些好处。

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