此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

SAML 2.0 登录概述

我们首先探讨 SAML 2.0 信赖方(Relying Party)身份验证在 Spring Security 中的工作原理。 首先,我们注意到,与OAuth 2.0 登录类似,Spring Security 会将用户重定向到第三方进行身份验证。 这是通过一系列重定向实现的:spring-doc.cadn.net.cn

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

number 1首先,用户向/private资源,但未获得授权访问该资源。spring-doc.cadn.net.cn

number 2Spring Security 的AuthorizationFilter表示未认证的请求是拒绝访问通过抛出AccessDeniedException.spring-doc.cadn.net.cn

number 3由于用户缺乏授权,ExceptionTranslationFilter启动开始身份验证. 已配置的AuthenticationEntryPointLoginUrlAuthenticationEntryPoint,将重定向至<saml2:AuthnRequest> 生成端点, Saml2WebSsoAuthenticationRequestFilter. 或者,如果您已经配置了多个断言方,它首先会重定向到选择器页面。spring-doc.cadn.net.cn

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

number 5然后浏览器会处理这个<saml2:AuthnRequest>并向主张方展示。 主张方尝试对用户进行身份验证。 如果成功,则返回<saml2:Response>返回浏览器。spring-doc.cadn.net.cn

number 6浏览器随后发送 POST 请求到<saml2:Response>到断言消费者服务端点。spring-doc.cadn.net.cn

下图展示了 Spring Security 如何对一个 <saml2:Response> 进行身份验证。spring-doc.cadn.net.cn

saml2webssoauthenticationfilter
图2. 对 <saml2:Response> 进行身份验证

该图基于我们的 SecurityFilterChain 图表构建。spring-doc.cadn.net.cn

number 1当浏览器提交时<saml2:Response>欢迎使用本应用,它委托给 Saml2WebSsoAuthenticationFilter. 此过滤器调用其配置的AuthenticationConverter创建Saml2AuthenticationToken通过从HttpServletRequest. 此转换器还会解析RelyingPartyRegistration并提供给Saml2AuthenticationToken.spring-doc.cadn.net.cn

number 2接下来,过滤器将Tokens传递给其配置的AuthenticationManager. 默认情况下,它使用OpenSaml4AuthenticationProvider.spring-doc.cadn.net.cn

number 3如果身份验证失败,则失败.spring-doc.cadn.net.cn

number 4如果认证成功,则成功.spring-doc.cadn.net.cn

最小化依赖

SAML 2.0 服务提供者支持位于 spring-security-saml2-service-provider 中。 它基于 OpenSAML 库构建,因此您还必须在构建配置中包含 Shibboleth Maven 仓库。 有关为何需要单独的仓库的更多详细信息,请查看此链接spring-doc.cadn.net.cn

<repositories>
    <!-- ... -->
    <repository>
        <id>shibboleth-releases</id>
        <name>Shibboleth Releases Repository</name>
        <url>https://build.shibboleth.net/maven/releases/</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </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-doc.cadn.net.cn

此外,此配置还假定您已经在您的断言方中注册了依赖方

指定身份提供商元数据

在 Spring Boot 应用程序中,要指定身份提供商的元数据,请创建类似如下的配置:spring-doc.cadn.net.cn

spring:
  security:
    saml2:
      relyingparty:
        registration:
          adfs:
            assertingparty:
              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

就这样了!spring-doc.cadn.net.cn

身份提供者(Identity Provider)与断言方(Asserting Party)是同义词,服务提供者(Service Provider)与依赖方(Relying Party)也是如此。 它们通常分别缩写为 AP 和 RP。spring-doc.cadn.net.cn

运行时期望

之前所配置,该应用程序会处理任何包含 POST /login/saml2/sso/{registrationId} 参数的 SAMLResponse 请求:spring-doc.cadn.net.cn

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

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

有两种方式可以促使你的断言方生成一个 SAMLResponsespring-doc.cadn.net.cn

  • 您可以导航到您的断言方。 它很可能为每个已注册的信赖方都提供某种链接或按钮,您可以点击该链接或按钮以发送 SAMLResponsespring-doc.cadn.net.cn

  • 您可以导航到应用程序中的受保护页面——例如,localhost:8080。 随后,您的应用程序会重定向到已配置的断言方(asserting party),该断言方将发送 SAMLResponsespring-doc.cadn.net.cn

从这里,可以考虑跳转到:spring-doc.cadn.net.cn

SAML 2.0 登录如何与 OpenSAML 集成

Spring Security 对 SAML 2.0 的支持有几个设计目标:spring-doc.cadn.net.cn

  • 依赖一个库来执行 SAML 2.0 操作和处理领域对象。 为此,Spring Security 使用了 OpenSAML。spring-doc.cadn.net.cn

  • 在使用 Spring Security 的 SAML 支持时,请确保不需要此库。 为实现这一点,Spring Security 在其接口或类中使用 OpenSAML 的部分均保持封装。 这使得您可以将 OpenSAML 替换为其他库或不受支持的 OpenSAML 版本。spring-doc.cadn.net.cn

作为这两个目标的自然结果,Spring Security 的 SAML API 相对于其他模块而言规模相当小。 取而代之的是,诸如 OpenSamlXAuthenticationRequestFactoryOpenSamlXAuthenticationProvider 这样的类会暴露 Converter 实现,用于自定义认证过程中的各个步骤。spring-doc.cadn.net.cn

例如,一旦您的应用程序接收到一个 SAMLResponse 并委托给 Saml2WebSsoAuthenticationFilter,该过滤器会进一步委托给 OpenSamlXAuthenticationProviderspring-doc.cadn.net.cn

验证 OpenSAML Response

opensamlauthenticationproviderspring-doc.cadn.net.cn

number 1Saml2WebSsoAuthenticationFilter制定了Saml2AuthenticationToken并调用AuthenticationManager.spring-doc.cadn.net.cn

number 2AuthenticationManager调用 OpenSAML 身份验证提供者。spring-doc.cadn.net.cn

number 3身份认证提供者将响应反序列化为 OpenSAMLResponse并验证其签名。 如果签名无效,则身份验证失败。spring-doc.cadn.net.cn

number 4然后提供者解密任何 EncryptedAssertion 元素. 如果任何解密失败,则身份验证失败。spring-doc.cadn.net.cn

number 5接下来,提供者将验证响应的IssuerDestination值。 如果它们与RelyingPartyRegistration,身份验证失败。spring-doc.cadn.net.cn

number 6之后,提供者验证每个Assertion. 如果任何签名无效,则身份验证失败。 此外,如果响应和断言都没有签名,则身份验证也会失败。 响应或所有断言中必须至少有一个包含签名。spring-doc.cadn.net.cn

number 7然后,提供者,解密任何EncryptedID or EncryptedAttribute元素]。 如果任何解密失败,则身份验证失败。spring-doc.cadn.net.cn

number 8接下来,提供者将验证每个断言的ExpiresAtNotBefore时间戳,<Subject>和任何<AudienceRestriction>条件。 如果任何验证失败,则认证失败。spring-doc.cadn.net.cn

number 9随后,提供者将第一个断言的AttributeStatement并将其映射到Map<String, List<Object>>. 它还授予了ROLE_USER授予权限。spring-doc.cadn.net.cn

number 10最后,它需要NameID根据第一个断言,Map的属性和,以及GrantedAuthority并构建Saml2AuthenticatedPrincipal. 然后,它将该主体和权限放入Saml2Authentication.spring-doc.cadn.net.cn

生成的 Authentication#getPrincipal 是一个 Spring Security Saml2AuthenticatedPrincipal 对象,且 Authentication#getName 映射到第一个断言的 NameID 元素。 Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId 包含指向关联 RelyingPartyRegistration标识符spring-doc.cadn.net.cn

自定义 OpenSAML 配置

任何同时使用 Spring Security 和 OpenSAML 的类都应在类的开头静态初始化 OpenSamlInitializationServicespring-doc.cadn.net.cn

static {
	OpenSamlInitializationService.initialize();
}
companion object {
    init {
        OpenSamlInitializationService.initialize()
    }
}

这取代了 OpenSAML 的 InitializationService#initializespring-doc.cadn.net.cn

有时,自定义 OpenSAML 构建、序列化(marshall)和反序列化(unmarshall)SAML 对象的方式会很有价值。 在这种情况下,你可能希望调用 OpenSamlInitializationService#requireInitialize(Consumer),以获得对 OpenSAML 的 XMLObjectProviderFactory 的访问权限。spring-doc.cadn.net.cn

例如,在发送未签名的 AuthNRequest 时,你可能希望强制重新认证。 在这种情况下,你可以注册自己的 AuthnRequestMarshaller,如下所示:spring-doc.cadn.net.cn

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 方法。spring-doc.cadn.net.cn

覆盖或替换 Boot 自动配置

Spring Boot 会为依赖方生成两个 @Bean 对象。spring-doc.cadn.net.cn

第一个是配置应用程序作为信赖方(relying party)的SecurityFilterChain。 当引入spring-security-saml2-service-provider时,该SecurityFilterChain如下所示:spring-doc.cadn.net.cn

默认 SAML 2.0 登录配置
@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 将会暴露上述默认的配置。spring-doc.cadn.net.cn

您可以通过在应用程序中暴露该 bean 来替换此项:spring-doc.cadn.net.cn

自定义 SAML 2.0 登录配置
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/messages/**").hasAuthority("ROLE_USER")
                .anyRequest().authenticated()
            )
            .saml2Login(withDefaults());
        return http.build();
    }
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration {
    @Bean
    open fun filterChain(http: HttpSecurity): SecurityFilterChain {
        http {
            authorizeRequests {
                authorize("/messages/**", hasAuthority("ROLE_USER"))
                authorize(anyRequest, authenticated)
            }
            saml2Login {
            }
        }
        return http.build()
    }
}

前面的示例要求任何以 USER 开头的 URL 都必须具有 /messages/ 角色。spring-doc.cadn.net.cn

Spring Boot 创建的第二个 @BeanRelyingPartyRegistrationRepository,它代表声明方和依赖方的元数据。 其中包括依赖方在向声明方请求身份验证时应使用的 SSO 端点位置等信息。spring-doc.cadn.net.cn

你可以通过发布自己的 RelyingPartyRegistrationRepository Bean 来覆盖默认配置。 例如,你可以通过访问断言方的元数据端点来查找其配置:spring-doc.cadn.net.cn

信赖方注册仓库
@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 是一个由您任意指定的值,用于区分不同的注册。

或者,您可以手动提供每个详细信息:spring-doc.cadn.net.cn

依赖方注册仓库手动配置
@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")
            .assertingPartyMetadata(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")
        .assertingPartyMetadata { party: AssertingPartyMetadata.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 类,在前面的代码片段中用于简化表达。spring-doc.cadn.net.cn

或者,您也可以直接使用 DSL 来装配该仓库,这同样会覆盖自动配置的 SecurityFilterChainspring-doc.cadn.net.cn

自定义信赖方注册 DSL
@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 中注册多个依赖方来实现多租户。spring-doc.cadn.net.cn

如果你想让元数据能够定期刷新,可以像下面这样将你的仓库包装在 CachingRelyingPartyRegistrationRepository 中:spring-doc.cadn.net.cn

缓存依赖方注册仓库
@Configuration
@EnableWebSecurity
public class MyCustomSecurityConfiguration {
    @Bean
    public RelyingPartyRegistrationRepository registrations(CacheManager cacheManager) {
		Supplier<IterableRelyingPartyRegistrationRepository> delegate = () ->
            new InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations
                .fromMetadataLocation("https://idp.example.org/ap/metadata")
                .registrationId("ap").build());
		CachingRelyingPartyRegistrationRepository registrations =
            new CachingRelyingPartyRegistrationRepository(delegate);
		registrations.setCache(cacheManager.getCache("my-cache-name"));
        return registrations;
    }
}
@Configuration
@EnableWebSecurity
class MyCustomSecurityConfiguration  {
    @Bean
    fun registrations(cacheManager: CacheManager): RelyingPartyRegistrationRepository {
        val delegate = Supplier<IterableRelyingPartyRegistrationRepository> {
             InMemoryRelyingPartyRegistrationRepository(RelyingPartyRegistrations
                .fromMetadataLocation("https://idp.example.org/ap/metadata")
                .registrationId("ap").build())
        }
        val registrations = CachingRelyingPartyRegistrationRepository(delegate)
        registrations.setCache(cacheManager.getCache("my-cache-name"))
        return registrations
    }
}

通过这种方式,RelyingPartyRegistration 的集合将根据缓存的淘汰策略进行刷新。spring-doc.cadn.net.cn

依赖方注册

一个 RelyingPartyRegistration 实例表示依赖方与断言方元数据之间的链接。spring-doc.cadn.net.cn

RelyingPartyRegistration 中,您可以提供信赖方元数据,例如其 Issuer 值、期望接收 SAML 响应的地址,以及用于对负载进行签名或解密的任何凭据。spring-doc.cadn.net.cn

此外,您还可以提供断言方的元数据,例如其 Issuer 值、它期望接收 AuthnRequests 请求的地址,以及它拥有的任何公钥凭证,供信赖方用于验证或加密有效载荷。spring-doc.cadn.net.cn

以下 RelyingPartyRegistration 是大多数配置所需的最低要求:spring-doc.cadn.net.cn

RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
        .fromMetadataLocation("https://ap.example.org/metadata")
        .registrationId("my-id")
        .build();
val relyingPartyRegistration = RelyingPartyRegistrations
    .fromMetadataLocation("https://ap.example.org/metadata")
    .registrationId("my-id")
    .build()

请注意,你也可以从任意的 RelyingPartyRegistration 源创建一个 InputStream。 一个这样的例子是当元数据存储在数据库中时:spring-doc.cadn.net.cn

String xml = fromDatabase();
try (InputStream source = new ByteArrayInputStream(xml.getBytes())) {
    RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistrations
            .fromMetadata(source)
            .registrationId("my-id")
            .build();
}

也可以进行更复杂的配置:spring-doc.cadn.net.cn

RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyMetadata(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}")
        .assertingPartyMetadata { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()

顶层元数据方法是关于信赖方的详细信息。 AssertingPartyMetadata 内部的方法是关于断言方的详细信息。spring-doc.cadn.net.cn

依赖方期望接收 SAML 响应的位置即为断言消费者服务位置(Assertion Consumer Service Location)。spring-doc.cadn.net.cn

依赖方(relying party)的 entityId 默认值为 {baseUrl}/saml2/service-provider-metadata/{registrationId}。 在配置断言方(asserting party)以使其了解您的依赖方时,需要使用此值。spring-doc.cadn.net.cn

assertionConsumerServiceLocation 的默认值为 /login/saml2/sso/{registrationId}。 默认情况下,它在过滤器链中映射到 Saml2WebSsoAuthenticationFilterspring-doc.cadn.net.cn

URI 模式

您可能已经注意到前面示例中的 {baseUrl}{registrationId} 占位符。spring-doc.cadn.net.cn

这些对于生成 URI 非常有用。因此,依赖方的 entityIdassertionConsumerServiceLocation 支持以下占位符:spring-doc.cadn.net.cn

例如,前面定义的 assertionConsumerServiceLocation 为:spring-doc.cadn.net.cn

/my-login-endpoint/{registrationId}spring-doc.cadn.net.cn

在已部署的应用程序中,它会转换为:spring-doc.cadn.net.cn

/my-login-endpoint/adfsspring-doc.cadn.net.cn

前面所示的 entityId 定义如下:spring-doc.cadn.net.cn

{baseUrl}/{registrationId}spring-doc.cadn.net.cn

在已部署的应用程序中,这表示为:spring-doc.cadn.net.cn

https://rp.example.com/adfsspring-doc.cadn.net.cn

主流的 URI 模式如下:spring-doc.cadn.net.cn

由于 registrationIdRelyingPartyRegistration 的主要标识符,因此在未认证场景中需要在 URL 中包含它。 如果您希望出于任何原因从 URL 中移除 registrationId,您可以指定一个 RelyingPartyRegistrationResolver,以告知 Spring Security 如何查找 registrationIdspring-doc.cadn.net.cn

凭证

在前面所示的示例中,您可能也注意到了所使用的凭证。spring-doc.cadn.net.cn

通常情况下,依赖方使用同一个密钥对有效载荷进行签名和解密。 或者,也可以使用同一个密钥来验证有效载荷以及对其进行加密。spring-doc.cadn.net.cn

因此,Spring Security 提供了 Saml2X509Credential,这是一个 SAML 专用的凭证类,可简化为不同使用场景配置同一密钥的过程。spring-doc.cadn.net.cn

至少,你需要拥有来自断言方的证书,以便验证断言方签名的响应。spring-doc.cadn.net.cn

要构造一个可用于验证断言方(asserting party)所发断言的 Saml2X509Credential,您可以加载该文件并使用 CertificateFactoryspring-doc.cadn.net.cn

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?
    )
}

假设断言方也将对断言进行加密。 在这种情况下,依赖方需要一个私钥来解密该加密值。spring-doc.cadn.net.cn

在这种情况下,你还需要一个 RSAPrivateKey 以及其对应的 X509Certificate。 你可以使用 Spring Security 的 RsaKeyConverters 工具类来加载前者,而后者则像之前那样加载即可:spring-doc.cadn.net.cn

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 会自动为你执行这些转换。spring-doc.cadn.net.cn

重复的依赖方配置

当应用程序使用多个断言方时,某些配置会在 RelyingPartyRegistration 实例之间重复:spring-doc.cadn.net.cn

与某些身份提供商相比,这种设置可能使其他身份提供商的凭据更容易轮换。spring-doc.cadn.net.cn

可以通过几种不同的方式来减轻这种重复。spring-doc.cadn.net.cn

首先,在 YAML 中可以通过引用缓解此问题:spring-doc.cadn.net.cn

spring:
  security:
    saml2:
      relyingparty:
        registration:
          okta:
            signing.credentials: &relying-party-credentials
              - private-key-location: classpath:rp.key
                certificate-location: classpath:rp.crt
            assertingparty:
              entity-id: ...
          azure:
            signing.credentials: *relying-party-credentials
            assertingparty:
              entity-id: ...

其次,在数据库中,你无需复制 RelyingPartyRegistration 的模型。spring-doc.cadn.net.cn

第三,在 Java 中,你可以创建一个自定义配置方法:spring-doc.cadn.net.cn

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 来解析 RelyingPartyRegistrationspring-doc.cadn.net.cn

根据使用场景的不同,还会采用多种其他策略来推导出一个。 例如:spring-doc.cadn.net.cn

  • 在处理 <saml2:Response> 时,RelyingPartyRegistration 会从关联的 <saml2:AuthRequest> 中查找,或者从 <saml2:Response#Issuer> 元素中查找。spring-doc.cadn.net.cn

  • 在处理 <saml2:LogoutRequest> 时,RelyingPartyRegistration 会从当前已登录的用户或 <saml2:LogoutRequest#Issuer> 元素中查找。spring-doc.cadn.net.cn

  • 为了发布元数据,RelyingPartyRegistration 会从任意同时实现了 Iterable<RelyingPartyRegistration> 的存储库中查找。spring-doc.cadn.net.cn

当需要进行调整时,您可以针对每个端点的具体组件进行定制化配置:spring-doc.cadn.net.cn

联合登录

SAML 2.0 的一种常见配置是,一个身份提供商(Identity Provider)拥有多个断言方(Asserting Party)。 在这种情况下,身份提供商的元数据端点会返回多个 <md:IDPSSODescriptor> 元素。spring-doc.cadn.net.cn

可以通过一次对 RelyingPartyRegistrations 的调用访问这些多个断言方,如下所示:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

在许多联合身份认证场景中,所有断言方共享相同的服务提供者配置。 鉴于 Spring Security 默认会在服务提供者元数据中包含 registrationId,下一步是修改相应的 URI 以排除 registrationId。这一点在上面的示例中已经完成,其中 entityIdassertionConsumerServiceLocation 均配置为静态端点。spring-doc.cadn.net.cn

您可以在我们的saml-extension-federation示例中查看此内容的完整示例。spring-doc.cadn.net.cn

使用 Spring Security SAML 扩展 URI

如果您正在从 Spring Security SAML 扩展进行迁移,将应用程序配置为使用 SAML 扩展的 URI 默认值可能会带来一些好处。spring-doc.cadn.net.cn