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 创建的第二个 @Bean 是一个 RelyingPartyRegistrationRepository,它代表着断言方和依赖方元数据。这包括诸如依赖方在向断言方请求身份验证时应使用的 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 中,您可以提供依赖方元数据,例如其 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 很有用。因此,信赖方的 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>RelyingPartyRegistration 是从关联的 <saml2:AuthRequest><saml2:Response#Issuer> 元素中查找的

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

  • 为了发布元数据,RelyingPartyRegistration 是从任何也实现 Iterable<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 示例