SAML 2.0 Login Overview

我们首先检查 SAML 2.0 依靠方身份验证如何在 Spring Security 中起作用。首先,我们看到,像 RFC 1901 一样,Spring Security 会将用户带到第三方执行身份验证。它通过一系列重定向来执行此操作:

We start by examining how SAML 2.0 Relying Party Authentication works within Spring Security. First, we see that, like oauth2login, Spring Security takes the user to a third party for performing authentication. It does this through a series of redirects: .Redirecting to Asserting Party Authentication image::servlet/saml2/saml2webssoauthenticationrequestfilter.png[]

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

The figure above builds off our SecurityFilterChain and AbstractAuthenticationProcessingFilter diagrams:

number 1 首先,用户向 /private 资源发送未经验证的请求,并且对该资源没有获得授权。

number 1 First, a user makes an unauthenticated request to the /private resource, for which it is not authorized.

number 2 Spring Security 的 AuthorizationFilter 通过抛出 AccessDeniedException 表明未经验证的请求是 Denied

number 2 Spring Security’s AuthorizationFilter indicates that the unauthenticated request is Denied by throwing an AccessDeniedException.

number 3由于用户缺乏授权,ExceptionTranslationFilter启动 Start Authentication。配置的 AuthenticationEntryPoint是 {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[LoginUrlAuthenticationEntryPoint] 的一个实例,它重定向到 the <saml2:AuthnRequest> generating endpointSaml2WebSsoAuthenticationRequestFilter。或者,如果您有 configured more than one asserting party,则它首先重定向到选择器页面。

number 3 Since the user lacks authorization, the ExceptionTranslationFilter initiates Start Authentication. The configured AuthenticationEntryPoint is an instance of {security-api-url}org/springframework/security/web/authentication/LoginUrlAuthenticationEntryPoint.html[LoginUrlAuthenticationEntryPoint], which redirects to servlet-saml2login-sp-initiated-factory, Saml2WebSsoAuthenticationRequestFilter. Alternatively, if you have servlet-saml2login-relyingpartyregistrationrepository, it first redirects to a picker page.

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

number 4 Next, the Saml2WebSsoAuthenticationRequestFilter creates, signs, serializes, and encodes a <saml2:AuthnRequest> using its configured <<`Saml2AuthenticationRequestFactory`,servlet-saml2login-sp-initiated-factory>>.

number 5然后浏览器将此 `<saml2:AuthnRequest>`带给验证方。验证方尝试验证用户。如果成功,则将 `<saml2:Response>`返回给浏览器。

number 5 Then the browser takes this <saml2:AuthnRequest> and presents it to the asserting party. The asserting party tries to authentication the user. If successful, it returns a <saml2:Response> back to the browser.

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

number 6 The browser then POSTs the <saml2:Response> to the assertion consumer service endpoint.

下图显示了 Spring Security 如何对 RFC 1901 进行身份验证。

The following image shows how Spring Security authenticates a <saml2:Response>.

saml2webssoauthenticationfilter
Figure 1. Authenticating a <saml2:Response>

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

The figure builds off our SecurityFilterChain diagram.

number 1当浏览器向应用程序提交 <saml2:Response>`时,它会 delegates to `Saml2WebSsoAuthenticationFilter。此筛选器调用其配置的 AuthenticationConverter`以通过从 `HttpServletRequest`中提取响应来创建 `Saml2AuthenticationToken。此转换器另外解析 <<`RelyingPartyRegistration`,servlet-saml2login-relyingpartyregistration>> 并将其提供给 Saml2AuthenticationToken

number 1 When the browser submits a <saml2:Response> to the application, it delegates to Saml2WebSsoAuthenticationFilter. This filter calls its configured AuthenticationConverter to create a Saml2AuthenticationToken by extracting the response from the HttpServletRequest. This converter additionally resolves the <<`RelyingPartyRegistration`,servlet-saml2login-relyingpartyregistration>> and supplies it to Saml2AuthenticationToken.

number 2接下来,筛选器将令牌传递给其配置的 AuthenticationManager。默认情况下,它使用 <<`OpenSamlAuthenticationProvider`,servlet-saml2login-architecture>>。

number 2 Next, the filter passes the token to its configured AuthenticationManager. By default, it uses the <<`OpenSamlAuthenticationProvider`,servlet-saml2login-architecture>>.

number 3如果验证失败,则 Failure

number 3 If authentication fails, then Failure.

number 4如果验证成功,则 Success

number 4 If authentication is successful, then Success.

  • The Authentication is set on the SecurityContextHolder.

  • The Saml2WebSsoAuthenticationFilter invokes FilterChain#doFilter(request,response) to continue with the rest of the application logic.

Minimal Dependencies

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

SAML 2.0 service provider support resides in spring-security-saml2-service-provider. It builds off of the OpenSAML library, and, for that reason, you must also include the Shibboleth Maven repository in your build configuration. Check this link for more details about why a separate repository is needed.

  • Maven

  • Gradle

<repositories>
    <!-- ... -->
    <repository>
        <id>shibboleth-releases</id>
        <url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
    </repository>
</repositories>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
repositories {
    // ...
    maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
}
dependencies {
    // ...
    implementation 'org.springframework.security:spring-security-saml2-service-provider'
}

Minimal Configuration

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

When using Spring Boot, configuring an application as a service provider consists of two basic steps: . Include the needed dependencies. . Indicate the necessary asserting party metadata.

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

Also, this configuration presupposes that you have already registered the relying party with your asserting party.

Specifying Identity Provider Metadata

在 Spring Boot 应用程序中,要指定身份提供程序的元数据,请创建类似于以下内容的配置:

In a Spring Boot application, to specify an identity provider’s metadata, create configuration similar to the following:

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

其中:

where:

这就是全部!

And that’s it!

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

Identity Provider and Asserting Party are synonymous, as are Service Provider and Relying Party. These are frequently abbreviated as AP and RP, respectively.

Runtime Expectations

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

As configured saml2-specifying-identity-provider-metadata, the application processes any POST /login/saml2/sso/{registrationId} request containing a SAMLResponse parameter:

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

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

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

There are two ways to induce your asserting party to generate a SAMLResponse:

  • You can navigate to your asserting party. It likely has some kind of link or button for each registered relying party that you can click to send the SAMLResponse.

  • You can navigate to a protected page in your application — for example, http://localhost:8080. Your application then redirects to the configured asserting party, which then sends the SAMLResponse.

从此处,考虑跳转到:

From here, consider jumping to:

How SAML 2.0 Login Integrates with OpenSAML

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

Spring Security’s SAML 2.0 support has a couple of design goals:

  • Rely on a library for SAML 2.0 operations and domain objects. To achieve this, Spring Security uses OpenSAML.

  • Ensure that this library is not required when using Spring Security’s SAML support. To achieve this, any interfaces or classes where Spring Security uses OpenSAML in the contract remain encapsulated. This makes it possible for you to switch out OpenSAML for some other library or an unsupported version of OpenSAML.

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

As a natural outcome of these two goals, Spring Security’s SAML API is quite small relative to other modules. Instead, such classes as OpenSamlAuthenticationRequestFactory and OpenSamlAuthenticationProvider expose Converter implementations that customize various steps in the authentication process.

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

For example, once your application receives a SAMLResponse and delegates to Saml2WebSsoAuthenticationFilter, the filter delegates to OpenSamlAuthenticationProvider:

Authenticating an OpenSAML Response

opensamlauthenticationprovider

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

This figure builds off of the <<`Saml2WebSsoAuthenticationFilter` diagram,servlet-saml2login-authentication-saml2webssoauthenticationfilter>>.

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

number 1 The Saml2WebSsoAuthenticationFilter formulates the Saml2AuthenticationToken and invokes the AuthenticationManager.

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

number 2 The AuthenticationManager invokes the OpenSAML authentication provider.

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

number 3 The authentication provider deserializes the response into an OpenSAML Response and checks its signature. If the signature is invalid, authentication fails.

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

number 4 Then the provider decrypts any EncryptedAssertion elements. If any decryptions fail, authentication fails.

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

number 5 Next, the provider validates the response’s Issuer and Destination values. If they do not match what’s in the RelyingPartyRegistration, authentication fails.

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

number 6 After that, the provider verifies the signature of each Assertion. If any signature is invalid, authentication fails. Also, if neither the response nor the assertions have signatures, authentication fails. Either the response or all the assertions must have signatures.

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

number 7 Then, the provider ,decrypts any EncryptedID or EncryptedAttribute elements]. If any decryptions fail, authentication fails.

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

number 8 Next, the provider validates each assertion’s ExpiresAt and NotBefore timestamps, the <Subject> and any <AudienceRestriction> conditions. If any validations fail, authentication fails.

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

number 9 Following that, the provider takes the first assertion’s AttributeStatement and maps it to a Map<String, List<Object>>. It also grants the ROLE_USER granted authority.

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

number 10 And finally, it takes the NameID from the first assertion, the Map of attributes, and the GrantedAuthority and constructs a Saml2AuthenticatedPrincipal. Then, it places that principal and the authorities into a Saml2Authentication.

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

The resulting Authentication#getPrincipal is a Spring Security Saml2AuthenticatedPrincipal object, and Authentication#getName maps to the first assertion’s NameID element. Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId holds the servlet-saml2login-relyingpartyregistrationid.

Customizing OpenSAML Configuration

同时使用 Spring Security 和 OpenSAML 的任何类都应在类的开头静态初始化 OpenSamlInitializationService

Any class that uses both Spring Security and OpenSAML should statically initialize OpenSamlInitializationService at the beginning of the class:

  • Java

  • Kotlin

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

这会替换 OpenSAML 的 InitializationService#initialize

This replaces OpenSAML’s InitializationService#initialize.

有时候,自定义 OpenSAML 构建、编组和反编组 SAML 对象的方式是很有价值的。在这种情况下,您可能希望调用 OpenSamlInitializationService#requireInitialize(Consumer),这会让您访问 OpenSAML 的 XMLObjectProviderFactory

Occasionally, it can be valuable to customize how OpenSAML builds, marshalls, and unmarshalls SAML objects. In these circumstances, you may instead want to call OpenSamlInitializationService#requireInitialize(Consumer) that gives you access to OpenSAML’s XMLObjectProviderFactory.

例如,在发送无符号 AuthNRequest 时,您可能希望强制重新认证。在该情况下,您可以注册您自己的 AuthnRequestMarshaller,如下所示:

For example, when sending an unsigned AuthNRequest, you may want to force reauthentication. In that case, you can register your own AuthnRequestMarshaller, like so:

  • 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 方法可以每个应用程序实例只调用一次。

The requireInitialize method may be called only once per application instance.

Overriding or Replacing Boot Auto Configuration

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

Spring Boot generates two @Bean objects for a relying party.

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

The first is a SecurityFilterChain that configures the application as a relying party. When including spring-security-saml2-service-provider, the SecurityFilterChain looks like:

Default SAML 2.0 Login Configuration
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .saml2Login(withDefaults());
    return http.build();
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeRequests {
            authorize(anyRequest, authenticated)
        }
        saml2Login { }
    }
    return http.build()
}

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

If the application does not expose a SecurityFilterChain bean, Spring Boot exposes the preceding default one.

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

You can replace this by exposing the bean within the application:

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 的角色。

The preceding example requires the role of USER for any URL that starts with /messages/.

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

The second @Bean Spring Boot creates is a {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistrationRepository.html[RelyingPartyRegistrationRepository], which represents the asserting party and relying party metadata. This includes such things as the location of the SSO endpoint the relying party should use when requesting authentication from the asserting party.

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

You can override the default by publishing your own RelyingPartyRegistrationRepository bean. For example, you can look up the asserting party’s configuration by hitting its metadata endpoint:

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

The registrationId is an arbitrary value that you choose for differentiating between registrations.

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

Alternatively, you can provide each detail manually:

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 类,在前面的代码段中为简洁而使用。

X509Support is an OpenSAML class, used in the preceding snippet for brevity.

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

Alternatively, you can directly wire up the repository by using the DSL, which also overrides the auto-configured 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 中注册多于一个依赖方而成为多租户。

A relying party can be multi-tenant by registering more than one relying party in the RelyingPartyRegistrationRepository.

RelyingPartyRegistration

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

A {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[RelyingPartyRegistration] instance represents a link between an relying party and an asserting party’s metadata.

RelyingPartyRegistration 中,您可以提供诸如 Issuer 值等依赖方元数据,它期望 SAML 响应发送给它,和它为签名或解密有效负载所拥有的所有证书。

In a RelyingPartyRegistration, you can provide relying party metadata like its Issuer value, where it expects SAML Responses to be sent to, and any credentials that it owns for the purposes of signing or decrypting payloads.

此外,您可以提供断言方元数据,如其`Issuer`值、它预期 AuthnRequests 被发送到的位置,以及它拥有的任何公共凭据,以供依赖方验证或加密有效负载。

Also, you can provide asserting party metadata like its Issuer value, where it expects AuthnRequests to be sent to, and any public credentials that it owns for the purposes of the relying party verifying or encrypting payloads.

以下 RelyingPartyRegistration 是大多数设置的最低要求:

The following RelyingPartyRegistration is the minimum required for most setups:

  • 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。一个这样的示例是当元数据存储在数据库中时:

Note that you can also create a RelyingPartyRegistration from an arbitrary InputStream source. One such example is when the metadata is stored in a database:

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

还可以进行更复杂的设置:

A more sophisticated setup is also possible:

  • 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 中的方法是关于断言方的详细信息。

The top-level metadata methods are details about the relying party. The methods inside assertingPartyDetails are details about the asserting party.

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

The location where a relying party is expecting SAML Responses is the Assertion Consumer Service Location.

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

The default for the relying party’s entityId is {baseUrl}/saml2/service-provider-metadata/{registrationId}. This is this value needed when configuring the asserting party to know about your relying party.

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

The default for the assertionConsumerServiceLocation is /login/saml2/sso/{registrationId}. By default, it is mapped to <<`Saml2WebSsoAuthenticationFilter`,servlet-saml2login-authentication-saml2webssoauthenticationfilter>> in the filter chain.

URI Patterns

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

You probably noticed the {baseUrl} and {registrationId} placeholders in the preceding examples.

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

These are useful for generating URIs. As a result, the relying party’s entityId and assertionConsumerServiceLocation support the following placeholders:

  • baseUrl - the scheme, host, and port of a deployed application

  • registrationId - the registration id for this relying party

  • baseScheme - the scheme of a deployed application

  • baseHost - the host of a deployed application

  • basePort - the port of a deployed application

例如,前面定义的 assertionConsumerServiceLocation 为:

For example, the assertionConsumerServiceLocation defined earlier was:

/my-login-endpoint/{registrationId}

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

In a deployed application, it translates to:

/my-login-endpoint/adfs

前面显示的 entityId 被定义为:

The entityId shown earlier was defined as:

{baseUrl}/{registrationId}

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

In a deployed application, that translates to:

当前的 URI 模式如下:

The prevailing URI patterns are as follows:

  • /saml2/authenticate/{registrationId} - The endpoint that generates a <saml2:AuthnRequest> based on the configurations for that RelyingPartyRegistration and sends it to the asserting party

  • /login/saml2/sso/ - The endpoint that authenticates an asserting party’s <saml2:Response>; the RelyingPartyRegistration is looked up from previously authenticated state or the response’s issuer if needed; also supports /login/saml2/sso/{registrationId}

  • /logout/saml2/sso - The endpoint that processes <saml2:LogoutRequest> and <saml2:LogoutResponse> payloads; the RelyingPartyRegistration is looked up from previously authenticated state or the request’s issuer if needed; also supports /logout/saml2/slo/{registrationId}

  • /saml2/metadata - The relying party metadata for the set of RelyingPartyRegistration`s; also supports `/saml2/metadata/{registrationId} or /saml2/service-provider-metadata/{registrationId} for a specific RelyingPartyRegistration

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

Since the registrationId is the primary identifier for a RelyingPartyRegistration, it is needed in the URL for unauthenticated scenarios. If you wish to remove the registrationId from the URL for any reason, you can servlet-saml2login-rpr-relyingpartyregistrationresolver to tell Spring Security how to look up the registrationId.

Credentials

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

In the example shown servlet-saml2login-relyingpartyregistration, you also likely noticed the credential that was used.

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

Oftentimes, a relying party uses the same key to sign payloads as well as decrypt them. Alternatively, it can use the same key to verify payloads as well as encrypt them.

因此,Spring Security随附`Saml2X509Credential`,这是一个专门用于 SAML 的凭证,它简化了在不同使用案例中配置相同密钥的操作。

Because of this, Spring Security ships with Saml2X509Credential, a SAML-specific credential that simplifies configuring the same key for different use cases.

至少,您需要来自断言方的证书,以便可以验证断言方的签名响应。

At a minimum, you need to have a certificate from the asserting party so that the asserting party’s signed responses can be verified.

要构建用于验证来自断言方的断言的`Saml2X509Credential`,可以加载文件并使用 CertificateFactory

To construct a Saml2X509Credential that you can use to verify assertions from the asserting party, you can load the file and use the 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?
    )
}

假设断言方也将加密断言。在这种情况下,依赖方需要私钥来解密加密值。

Suppose that the asserting party is going to also encrypt the assertion. In that case, the relying party needs a private key to decrypt the encrypted value.

在这种情况下,您需要 RSAPrivateKey 及其对应的 X509Certificate。可以使用 Spring Security 的 RsaKeyConverters 实用程序类加载第一个,并按照之前的做法加载第二个:

In that case, you need an RSAPrivateKey as well as its corresponding X509Certificate. You can load the first by using Spring Security’s RsaKeyConverters utility class and the second as you did before:

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

When you specify the locations of these files as the appropriate Spring Boot properties, Spring Boot performs these conversions for you.

Duplicated Relying Party Configurations

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

When an application uses multiple asserting parties, some configuration is duplicated between RelyingPartyRegistration instances:

  • The relying party’s entityId

  • Its assertionConsumerServiceLocation

  • Its credentials — for example, its signing or decryption credentials

对于某些标识提供程序与其他标识提供程序相比,这种设置可以更容易地轮换凭证。

This setup may let credentials be more easily rotated for some identity providers versus others.

可以通过一些不同的方式来缓解重复。

The duplication can be alleviated in a few different ways.

首先,在 YAML 中,可以使用引用来缓解这种情况:

First, in YAML this can be alleviated with references:

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 的模型。

Second, in a database, you need not replicate the model of RelyingPartyRegistration.

第三,在 Java 中,您可以创建自定义配置方法:

Third, in Java, you can create a custom configuration method:

  • Java

  • Kotlin

private RelyingPartyRegistration.Builder
        addRelyingPartyDetails(RelyingPartyRegistration.Builder builder) {

    Saml2X509Credential signingCredential = ...
    builder.signingX509Credentials(c -> c.addAll(signingCredential));
    // ... other relying party configurations
}

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration okta = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("okta")).build();

    RelyingPartyRegistration azure = addRelyingPartyDetails(
            RelyingPartyRegistrations
                .fromMetadataLocation(oktaMetadataUrl)
                .registrationId("azure")).build();

    return new InMemoryRelyingPartyRegistrationRepository(okta, azure);
}
private fun addRelyingPartyDetails(builder: RelyingPartyRegistration.Builder): RelyingPartyRegistration.Builder {
    val signingCredential: Saml2X509Credential = ...
    builder.signingX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
        c.add(
            signingCredential
        )
    }
    // ... other relying party configurations
}

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val okta = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("okta")
    ).build()
    val azure = addRelyingPartyDetails(
        RelyingPartyRegistrations
            .fromMetadataLocation(oktaMetadataUrl)
            .registrationId("azure")
    ).build()
    return InMemoryRelyingPartyRegistrationRepository(okta, azure)
}

Resolving the RelyingPartyRegistration from the Request

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

As seen so far, Spring Security resolves the RelyingPartyRegistration by looking for the registration id in the URI path.

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

Depending on the use case, a number of other strategies are also employed to derive one. For example:

  • For processing <saml2:Response>`s, the `RelyingPartyRegistration is looked up from the associated <saml2:AuthRequest> or from the <saml2:Response#Issuer> element

  • For processing <saml2:LogoutRequest>`s, the `RelyingPartyRegistration is looked up from the currently logged in user or from the <saml2:LogoutRequest#Issuer> element

  • For publishing metadata, the RelyingPartyRegistration`s are looked up from any repository that also implements `Iterable<RelyingPartyRegistration>

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

When this needs adjustment, you can turn to the specific components for each of these endpoints targeted at customizing this:

  • For SAML Responses, customize the AuthenticationConverter

  • For Logout Requests, customize the Saml2LogoutRequestValidatorParametersResolver

  • For Metadata, customize the Saml2MetadataResponseResolver

Federating Login

SAML 2.0 的一种常见设置是具有多个断言方的标识提供程序。在这种情况下,标识提供程序的元数据端点返回多个 <md:IDPSSODescriptor> 元素。

One common arrangement with SAML 2.0 is an identity provider that has multiple asserting parties. In this case, the identity provider’s metadata endpoint returns multiple <md:IDPSSODescriptor> elements.

可以使用以下方式以单个调用 RelyingPartyRegistrations 访问这些多个断言方:

These multiple asserting parties can be accessed in a single call to RelyingPartyRegistrations like so:

  • 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 端点变得不可预测。有几种方法可以解决此问题;我们重点关注一种适合联合特定使用案例的方法。

Note that because the registration id is set to a random value, this will change certain SAML 2.0 endpoints to be unpredictable. There are several ways to address this; let’s focus on a way that suits the specific use case of federation.

在很多联合体案例中,所有断言方都共享服务提供商配置。由于 Spring Security 将默认在服务提供商元数据中包含 registrationId,另一步骤是更改对应的 URI 以排除 registrationId,您可以在上面样例中看到已完成该步骤,其中 entityIdassertionConsumerServiceLocation 配置有静态端点。

In many federation cases, all the asserting parties share service provider configuration. Given that Spring Security will by default include the registrationId in the service provider metadata, another step is to change corresponding URIs to exclude the registrationId, which you can see has already been done in the above sample where the entityId and assertionConsumerServiceLocation are configured with a static endpoint.

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

You can see a completed example of this in our saml-extension-federation sample.

Using Spring Security SAML Extension URIs

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

In the event that you are migrating from the Spring Security SAML Extension, there may be some benefit to configuring your application to use the SAML Extension URI defaults.

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

For more information on this, please see our custom-urls sample and our saml-extension-federation sample.