SAML 2.0 登录概览

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

我们先来看看SAML 2.0依赖方认证在Spring Security中是如何工作的。首先,我们看到,像OAuth 2.0 登录一样,Spring Security 将用户带到第三方进行认证。它通过一系列的重定向来做到这一点。

saml2webssoauthenticationrequestfilter
Figure 1. Redirecting to Asserting Party Authentication

上图建立在我们的 SecurityFilterChainAbstractAuthenticationProcessingFilter 图之上。

number 1 首先,一个用户向 /private 资源发出一个未经认证的请求,而它没有得到授权。

number 2 Spring Security 的 AuthorizationFilter 通过抛出一个 AccessDeniedException 来表明未经认证的请求被拒绝了(Denied)。

number 3 由于用户没有授权,ExceptionTranslationFilter 启动了 Start Authentication。配置的 AuthenticationEntryPointLoginUrlAuthenticationEntryPoint 的一个实例,它重定向到 <saml2:AuthnRequest> 生成端点Saml2WebSsoAuthenticationRequestFilter。或者,如果你配置了一个以上的断言方(asserting party),它首先会重定向到一个挑选器(picker)页面。

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

number 5 然后,浏览器接受这个 <saml2:AuthnRequest> 并将其提交给断言方。断言方尝试认证用户。如果成功的话,它将返回一个 <saml2:Response> 给浏览器。

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

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

saml2webssoauthenticationfilter
Figure 2. Authenticating a <saml2:Response>

该图建立在我们的 SecurityFilterChain 图上。

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

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

number 3 如果认证失败,则为 Failure

number 4 如果认证成功,则 Success

  • Authentication 被设置在 SecurityContextHolder 上。

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

最小依赖

SAML 2.0服务提供者支持在 spring-security-saml2-service-provider 中。它建立在OpenSAML库的基础上。

最小配置

在使用 Spring Boot 时,将一个应用程序配置为一个服务提供者包括两个基本步骤。

  1. 添加所需的依赖。

  2. 指定必要的断言方元数据。

另外,这种配置的前提是,你 已经在你的断言方注册了依赖方

指定身份提供者(Identity Provider)元数据

在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操作和领域对象(domain object)。为了实现这一目标,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 的基础上绘制的。

number 1 Saml2WebSsoAuthenticationFilter 制定 Saml2AuthenticationToken 并调用 AuthenticationManager

number 2 AuthenticationManager 调用 OpenSAML authentication 提供者。

number 3 认证(authentication)提供者将响应反序列化为OpenSAML Response 并检查其签名。如果签名无效,认证失败。

number 4 然后,提供者 对任何 EncryptedAssertion 元素进行解密。如果任何解密失败,则认证失败。

number 5 接下来,提供者会验证响应的 Issuer 和目 Destination 的值。如果它们与 RelyingPartyRegistration 中的内容不一致,则验证失败。

number 6 之后,提供者会验证每个 Assertion 的签名。如果任何签名无效,认证失败。另外,如果 response 和 assertion 都没有签名,则认证失败。response 或所有 assertion 必须有签名。

number 7 然后,提供者 对任何 EncryptedIDEncryptedAttribute 元素进行解密。如果任何解密失败,则认证失败。

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

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

number 10 最后,它从第一个断言中获取 NameID、attribute MapGrantedAuthority,并构建一个 Saml2AuthenticatedPrincipal。然后,它将该委托人(principal)和授权放入一个 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 看起来像:

Default JWT 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创建的第二个 @BeanRelyingPartyRegistrationRepository,它表示断言方和依赖方的元数据。这包括诸如依赖方在向断言方请求认证时应使用的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直接连接到版本库,这也会覆盖自动配置的 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

RelyingPartyRegistration 实例表示依赖方和断言方元数据之间的联系。

RelyingPartyRegistration 中,你可以提供依赖方元数据,如它的 Issuer 值,它期望 SAML Response 被发送到哪里,以及它拥有的用于签署或解密有效payload的任何凭证。

另外,你可以提供断言方元数据,如它的 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 Response 的位置是断言消费者服务位置。

依赖方的 entityId 默认为 {baseUrl}/saml2/service-provider-metadata/{registrationId}。这是配置断言方了解你的依赖方时需要的这个值。

assertionConsumerServiceLocation 的默认值是 /login/saml2/sso/{registrationId}。默认情况下,它被映射到过滤器链中的 Saml2WebSsoAuthenticationFilter

URI Pattern

你可能注意到前面的例子中的 {baseUrl}{registrationId} 占位符。

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

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

  • registrationId - 该依赖方的registration id

  • baseScheme - 所部署的应用程序的scheme

  • baseHost - 所部署应用程序的host

  • basePort - 所部署应用程序的port

例如,前面定义的 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 从先前认证的状态中查找,或在需要时从 Response 的 issuer 中查找;也支持 /login/saml2/sso/{registrationId}

  • /logout/saml2/sso - 处理 <saml2:LogoutRequest><saml2:LogoutResponse> payload 的端点;RelyingPartyRegistration 将从以前的认证状态或请求的 issuer(如果需要)中查找;还支持 /logout/saml2/slo/{registrationId}

  • /saml2/metadata - RelyingPartyRegistration 集合的 可信赖方元数据;也支持针对特定 RelyingPartyRegistration/saml2/metadata/{registrationId}/saml2/service-provider-metadata/{registrationId}

由于 registrationIdRelyingPartyRegistration 的主要标识符,因此在未验证的情况下需要在 URL 中使用该标识符。如果出于任何原因希望从 URL 中移除 registrationId,可以 指定一个 RelyingPartyRegistrationResolver 来告诉 Spring Security 如何查找 registrationId

凭证(Credentials)

前面的例子中,你可能也注意到了所使用的凭证。

通常情况下,依赖方使用相同的密钥来签署有效载荷和解密它们。或者,它也可以用同一个key来验证有效载荷,并对其进行加密。

正因为如此,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 propertie 时,Spring Boot 会为你执行这些转换。

重复的 Relying Party 配置

当一个应用程序使用多个断言方时,一些配置会在 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中,你可以创建一个自定义的 configuration 方法。

  • 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> 的 repository 中查找。

当需要调整时,你可以针对每个端点的特定组件进行定制:

  • 对于 SAML Responses,自定义 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默认值可能会有一些好处。