SAML 2.0 登入概述

我們首先探討 SAML 2.0 Relying Party 認證在 Spring Security 中的工作原理。首先,我們看到,與 OAuth 2.0 登入類似,Spring Security 會將使用者引導到第三方進行認證。這是透過一系列重定向完成的。

saml2webssoauthenticationrequestfilter
圖 1. 重定向到 Asserting Party 進行認證

數字 1 首先,使用者對 /private 資源發出未經認證的請求,該請求未被授權。

數字 2 Spring Security 的 AuthorizationFilter 透過丟擲 AccessDeniedException 表明未經認證的請求被 *拒絕*(Denied)。

數字 3 由於使用者缺乏授權,ExceptionTranslationFilter 啟動*開始認證*(Start Authentication)。配置的 AuthenticationEntryPointLoginUrlAuthenticationEntryPoint 的一個例項,它重定向到生成 <saml2:AuthnRequest> 的端點 Saml2WebSsoAuthenticationRequestFilter。或者,如果您配置了多個 asserting party,它首先會重定向到一個選擇頁面。

數字 4 接下來,Saml2WebSsoAuthenticationRequestFilter 使用其配置的Saml2AuthenticationRequestFactory 建立、簽名、序列化和編碼一個 <saml2:AuthnRequest>

數字 5 然後瀏覽器接收此 <saml2:AuthnRequest> 並將其傳送給 asserting party。asserting party 嘗試認證使用者。如果成功,它會將一個 <saml2:Response> 返回給瀏覽器。

數字 6 然後瀏覽器將 <saml2:Response> 透過 POST 方法傳送到 assertion consumer service 端點。

下圖展示了 Spring Security 如何認證一個 <saml2:Response>

saml2webssoauthenticationfilter
圖 2. 認證一個 <saml2:Response>

此圖基於我們的 SecurityFilterChain 圖。

數字 1 當瀏覽器將 <saml2:Response> 提交到應用程式時,它會委託給 Saml2WebSsoAuthenticationFilter。此過濾器會呼叫其配置的 AuthenticationConverter,透過從 HttpServletRequest 中提取響應來建立一個 Saml2AuthenticationToken。此轉換器還會解析 RelyingPartyRegistration 並將其提供給 Saml2AuthenticationToken

數字 2 接下來,過濾器將令牌傳遞給其配置的 AuthenticationManager。預設情況下,它使用 OpenSamlAuthenticationProvider

數字 3 如果認證失敗,則為*失敗*(Failure)。

數字 4 如果認證成功,則為*成功*(Success)。

  • Authentication 設定到 SecurityContextHolder 上。

  • Saml2WebSsoAuthenticationFilter 呼叫 FilterChain#doFilter(request,response) 以繼續執行應用程式的其餘邏輯。

最低依賴

SAML 2.0 service provider 支援位於 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 時,將應用程式配置為 service provider 包括兩個基本步驟:
. 包含所需的依賴。
. 指明必要的 asserting party 元資料。

此外,此配置預設您已在 asserting party 中註冊了 relying party。

指定 Identity Provider 元資料

在 Spring Boot 應用中,要指定 identity provider 的元資料,請建立類似如下的配置

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

其中

就這樣!

Identity Provider 與 Asserting Party 是同義詞,Service Provider 與 Relying Party 也是同義詞。它們通常分別縮寫為 AP 和 RP。

執行時期望

正如之前配置的,應用程式會處理任何包含 SAMLResponse 引數的 POST /login/saml2/sso/{registrationId} 請求

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

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

有兩種方法可以促使您的 asserting party 生成 SAMLResponse

  • 您可以導航到您的 asserting party。它很可能為每個註冊的 relying party 提供了某種連結或按鈕,您可以點選它們來發送 SAMLResponse

  • 您可以導航到應用程式中的受保護頁面 — 例如 localhost:8080。然後您的應用程式會重定向到配置的 asserting party,asserting party 再發送 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

數字 1 Saml2WebSsoAuthenticationFilter 構建 Saml2AuthenticationToken 並呼叫 AuthenticationManager

數字 2 AuthenticationManager 呼叫 OpenSAML 認證提供者。

數字 3 認證提供者將響應反序列化為 OpenSAML Response 並檢查其簽名。如果簽名無效,認證失敗。

數字 4 然後提供者解密任何 EncryptedAssertion 元素。如果任何解密失敗,認證失敗。

數字 5 接下來,提供者驗證響應的 IssuerDestination 值。如果它們與 RelyingPartyRegistration 中的值不匹配,認證失敗。

數字 6 之後,提供者驗證每個 Assertion 的簽名。如果任何簽名無效,認證失敗。此外,如果響應和斷言都沒有簽名,認證也會失敗。響應或所有斷言都必須有簽名。

數字 7 然後,提供者解密任何 EncryptedIDEncryptedAttribute 元素。如果任何解密失敗,認證失敗。

數字 8 接下來,提供者驗證每個斷言的 ExpiresAtNotBefore 時間戳,<Subject> 和任何 <AudienceRestriction> 條件。如果任何驗證失敗,認證失敗。

數字 9 之後,提供者獲取第一個斷言的 AttributeStatement 並將其對映到 Map<String, List<Object>>。它還會授予 ROLE_USER 授權。

數字 10 最後,它獲取第一個斷言中的 NameID、屬性的 Map 以及 GrantedAuthority,並構建一個 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 會為 relying party 生成兩個 @Bean 物件。

第一個是 SecurityFilterChain,它將應用程式配置為 relying party。當包含 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,它表示 asserting party 和 relying party 的元資料。這包括 relying party 在向 asserting party 請求認證時應使用的 SSO 端點位置等資訊。

您可以透過釋出自己的 RelyingPartyRegistrationRepository bean 來覆蓋預設設定。例如,您可以透過訪問 asserting party 的元資料端點來查詢其配置

Relying Party Registration 倉庫
  • 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 倉庫手動配置
  • 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")
            .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 類,在前面的程式碼片段中使用是為了簡潔。

或者,您可以使用 DSL 直接配置倉庫,這也會覆蓋自動配置的 SecurityFilterChain

自定義 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 中註冊多個 relying party,一個 relying party 可以支援多租戶。

如果您希望元資料可以定期重新整理,您可以像這樣將您的倉庫包裝在 CachingRelyingPartyRegistrationRepository

快取 Relying Party Registration 倉庫
  • Java

  • Kotlin

@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 集合將根據快取的過期策略進行重新整理。

RelyingPartyRegistration

一個 RelyingPartyRegistration 例項代表 relying party 和 asserting party 元資料之間的連結。

RelyingPartyRegistration 中,您可以提供 relying party 元資料,例如其 Issuer 值、期望接收 SAML Responses 的位置,以及用於簽名或解密有效載荷的任何憑據。

此外,您可以提供 asserting party 元資料,例如其 Issuer 值、期望接收 AuthnRequests 的位置,以及 relying party 用於驗證或加密有效載荷的任何公共憑據。

以下 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}")
        .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()

頂層元資料方法是關於 relying party 的詳細資訊。AssertingPartyMetadata 內部的方法是關於 asserting party 的詳細資訊。

relying party 期望接收 SAML Responses 的位置是 Assertion Consumer Service Location。

relying party 的 entityId 預設值為 {baseUrl}/saml2/service-provider-metadata/{registrationId}。在配置 asserting party 以瞭解您的 relying party 時,需要這個值。

assertionConsumerServiceLocation 的預設值為 /login/saml2/sso/{registrationId}。預設情況下,它在過濾器鏈中對映到 Saml2WebSsoAuthenticationFilter

URI 模式

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

這些佔位符對於生成 URI 很有用。因此,relying party 的 entityIdassertionConsumerServiceLocation 支援以下佔位符

  • baseUrl - 已部署應用程式的方案(scheme)、主機(host)和埠(port)

  • registrationId - 此 relying party 的註冊 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> 並將其傳送到 asserting party 的端點

  • /login/saml2/sso/ - 認證 asserting party 的 <saml2:Response> 的端點;如果需要,RelyingPartyRegistration 會從之前認證的狀態或響應的 issuer 中查詢;也支援 /login/saml2/sso/{registrationId}

  • /logout/saml2/sso - 處理 <saml2:LogoutRequest><saml2:LogoutResponse> 有效載荷的端點;如果需要,RelyingPartyRegistration 會從當前登入使用者或請求的 issuer 中查詢;也支援 /logout/saml2/slo/{registrationId}

  • /saml2/metadata - RelyingPartyRegistration 集合的 relying party 元資料;對於特定的 RelyingPartyRegistration,也支援 /saml2/metadata/{registrationId}/saml2/service-provider-metadata/{registrationId}

由於 registrationIdRelyingPartyRegistration 的主要識別符號,在未經認證的場景中,URL 中需要它。如果出於任何原因您希望從 URL 中移除 registrationId,可以指定一個 RelyingPartyRegistrationResolver 來告訴 Spring Security 如何查詢 registrationId

憑據

前面展示的示例中,您可能也注意到了使用的憑據。

通常,relying party 使用相同的金鑰來簽名有效載荷和解密它們。或者,它也可以使用相同的金鑰來驗證有效載荷和加密它們。

因此,Spring Security 提供了 Saml2X509Credential,這是一種 SAML 專用的憑據,簡化了為不同用例配置相同金鑰的過程。

至少,您需要擁有 asserting party 的證書,以便驗證 asserting party 的簽名響應。

要構建可用於驗證 asserting party 斷言的 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?
    )
}

假設 asserting party 也要加密斷言。在這種情況下,relying party 需要一個私鑰來解密加密的值。

在這種情況下,您需要一個 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 會為您執行這些轉換。

重複的 Relying Party 配置

當一個應用程式使用多個 asserting parties 時,一些配置會在 RelyingPartyRegistration 例項之間重複

  • relying party 的 entityId

  • assertionConsumerServiceLocation

  • 其憑據 — 例如,其簽名或解密憑據

這種設定可能使得某些 identity provider 的憑據比其他更容易輪換。

可以通過幾種不同的方式來緩解重複問題。

首先,在 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 路徑中的 registration id 來解析 RelyingPartyRegistration

根據用例,還採用了許多其他策略來派生它。例如

  • 對於處理 <saml2:Response>RelyingPartyRegistration 從關聯的 <saml2:AuthRequest> 或從 <saml2:Response#Issuer> 元素中查詢

  • 對於處理 <saml2:LogoutRequest>RelyingPartyRegistration 從當前登入使用者或從 <saml2:LogoutRequest#Issuer> 元素中查詢

  • 對於釋出元資料,RelyingPartyRegistration 會從任何實現了 Iterable<RelyingPartyRegistration> 的倉庫中查詢

當需要調整時,您可以轉向針對定製此功能的各個端點的特定元件

  • 對於 SAML Responses,定製 AuthenticationConverter

  • 對於 Logout Requests,定製 Saml2LogoutRequestValidatorParametersResolver

  • 對於 Metadata,定製 Saml2MetadataResponseResolver

聯合登入

SAML 2.0 中一個常見的配置是一個 identity provider 擁有多個 asserting parties。在這種情況下,identity provider 的元資料端點會返回多個 <md:IDPSSODescriptor> 元素。

可以透過一次呼叫 RelyingPartyRegistrations 來訪問這些多個 asserting parties,如下所示

  • 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())

請注意,由於 registration id 設定為隨機值,這將導致某些 SAML 2.0 端點變得不可預測。有幾種方法可以解決此問題;讓我們關注一種適用於聯合特定用例的方法。

在許多聯合場景中,所有 asserting parties 共享 service provider 配置。鑑於 Spring Security 預設會在 service provider 元資料中包含 registrationId,另一個步驟是將相應的 URI 更改為排除 registrationId,您可以看到在上面的示例中已經這樣做了,其中 entityIdassertionConsumerServiceLocation 配置為靜態端點。

您可以在我們的 saml-extension-federation 示例中看到一個完整的示例。

使用 Spring Security SAML 擴充套件 URI

如果您正在從 Spring Security SAML 擴充套件遷移,配置應用程式使用 SAML 擴充套件預設 URI 可能會帶來一些好處。

有關此內容的更多資訊,請參閱我們的 custom-urls 示例和我們的 saml-extension-federation 示例