SAML 2.0 登入概觀

我們先從檢視 SAML 2.0 Relying Party 身份驗證如何在 Spring Security 內運作開始。首先,我們看到,如同 OAuth 2.0 登入,Spring Security 會將使用者導向至第三方以執行身份驗證。它透過一系列的重新導向來達成此目的。

saml2webssoauthenticationrequestfilter
圖 1. 重新導向至宣告方身份驗證

上圖是建立在我們的 SecurityFilterChainAbstractAuthenticationProcessingFilter 圖表之上。

number 1 首先,使用者對 /private 資源發出未經身份驗證的請求,但使用者未被授權存取該資源。

number 2 Spring Security 的 AuthorizationFilter 表示未經身份驗證的請求被拒絕,並拋出 AccessDeniedException

number 3 由於使用者缺乏授權,ExceptionTranslationFilter 啟動開始身份驗證。已組態的 AuthenticationEntryPointLoginUrlAuthenticationEntryPoint 的實例,它會重新導向至 產生 <saml2:AuthnRequest> 端點Saml2WebSsoAuthenticationRequestFilter。或者,如果您已組態多個宣告方,它會先重新導向至選擇器頁面。

number 4 接著,Saml2WebSsoAuthenticationRequestFilter 使用其已組態的 Saml2AuthenticationRequestFactory 建立、簽署、序列化和編碼 <saml2:AuthnRequest>

number 5 然後,瀏覽器取得此 <saml2:AuthnRequest> 並將其呈現給宣告方。宣告方嘗試驗證使用者身份。如果成功,它會將 <saml2:Response> 回應給瀏覽器。

number 6 瀏覽器接著將 <saml2:Response> POST 到斷言消費者服務端點。

下圖顯示 Spring Security 如何驗證 <saml2:Response>

saml2webssoauthenticationfilter
圖 2. 驗證 <saml2:Response>

此圖是建立在我們的 SecurityFilterChain 圖表之上。

number 1 當瀏覽器將 <saml2:Response> 提交到應用程式時,它會委派給 Saml2WebSsoAuthenticationFilter。此篩選器呼叫其已組態的 AuthenticationConverter,以透過從 HttpServletRequest 提取回應來建立 Saml2AuthenticationToken。此轉換器還會解析 RelyingPartyRegistration 並將其提供給 Saml2AuthenticationToken

number 2 接著,篩選器將權杖傳遞給其已組態的 AuthenticationManager。預設情況下,它使用 OpenSamlAuthenticationProvider

number 3 如果身份驗證失敗,則為失敗

number 4 如果身份驗證成功,則為成功

  • 會在 SecurityContextHolder 上設定 Authentication

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

最低相依性

SAML 2.0 服務提供者支援位於 spring-security-saml2-service-provider 中。它建立在 OpenSAML 程式庫之上,因此,您還必須在組建組態中包含 Shibboleth Maven 儲存庫。請查看此連結,以瞭解為何需要單獨的儲存庫的更多詳細資訊。

  • Maven

  • Gradle

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

最低組態

當使用 Spring Boot 時,將應用程式組態為服務提供者包含兩個基本步驟:. 包含所需的相依性。 . 指示必要的宣告方 Metadata。

此外,此組態以前提是您已向宣告方註冊了 Relying Party

指定身份提供者 Metadata

在 Spring Boot 應用程式中,若要指定身份提供者的 Metadata,請建立類似於以下的組態

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

其中

就是這樣!

身份提供者和宣告方是同義詞,服務提供者和 Relying Party 也是同義詞。這些經常縮寫為 AP 和 RP。

執行階段預期

先前組態的,應用程式會處理任何包含 SAMLResponse 參數的 POST /login/saml2/sso/{registrationId} 請求。

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

SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...

有兩種方法可以誘導宣告方產生 SAMLResponse

  • 您可以導航至宣告方。它可能具有某種連結或按鈕,適用於您可以點擊的每個已註冊的 Relying Party,以傳送 SAMLResponse

  • 您可以導航至應用程式中的受保護頁面 — 例如,localhost:8080。您的應用程式接著會重新導向至已組態的宣告方,然後宣告方會傳送 SAMLResponse

從這裡開始,考慮跳轉到

SAML 2.0 登入如何與 OpenSAML 整合

Spring Security 的 SAML 2.0 支援有幾個設計目標。

  • 依賴程式庫進行 SAML 2.0 操作和網域物件。為了實現此目的,Spring Security 使用 OpenSAML。

  • 確保在使用 Spring Security 的 SAML 支援時,不需要此程式庫。為了實現此目的,Spring Security 在合約中使用 OpenSAML 的任何介面或類別都保持封裝。這使您可以切換出 OpenSAML 以使用其他程式庫或不受支援版本的 OpenSAML。

作為這兩個目標的自然結果,相對於其他模組,Spring Security 的 SAML API 非常小。相反,諸如 OpenSamlAuthenticationRequestFactoryOpenSamlAuthenticationProvider 之類的類別公開了 Converter 實作,這些實作自訂了身份驗證程序中的各個步驟。

例如,一旦您的應用程式收到 SAMLResponse 並委派給 Saml2WebSsoAuthenticationFilter,篩選器就會委派給 OpenSamlAuthenticationProvider

驗證 OpenSAML Response

opensamlauthenticationprovider

此圖建立在 Saml2WebSsoAuthenticationFilter 圖表之上。

number 1 Saml2WebSsoAuthenticationFilter 公式化 Saml2AuthenticationToken 並調用 AuthenticationManager

number 2 AuthenticationManager 調用 OpenSAML 身份驗證提供者。

number 3 身份驗證提供者將回應反序列化為 OpenSAML Response 並檢查其簽章。如果簽章無效,則身份驗證失敗。

number 4 然後,提供者解密任何 EncryptedAssertion 元素。如果任何解密失敗,則身份驗證失敗。

number 5 接著,提供者驗證回應的 IssuerDestination 值。如果它們與 RelyingPartyRegistration 中的內容不符,則身份驗證失敗。

number 6 之後,提供者驗證每個 Assertion 的簽章。如果任何簽章無效,則身份驗證失敗。此外,如果回應和斷言都沒有簽章,則身份驗證失敗。回應或所有斷言都必須具有簽章。

number 7 然後,提供者,解密任何 EncryptedIDEncryptedAttribute 元素]。如果任何解密失敗,則身份驗證失敗。

number 8 接著,提供者驗證每個斷言的 ExpiresAtNotBefore 時間戳記、<Subject> 和任何 <AudienceRestriction> 條件。如果任何驗證失敗,則身份驗證失敗。

number 9 接下來,提供者取得第一個斷言的 AttributeStatement 並將其映射到 Map<String, List<Object>>。它還授予 ROLE_USER 授權。

number 10 最後,它從第一個斷言中取得 NameID、屬性的 MapGrantedAuthority 並建構 Saml2AuthenticatedPrincipal。然後,它將該主體和授權放入 Saml2Authentication

產生的 Authentication#getPrincipal 是 Spring Security Saml2AuthenticatedPrincipal 物件,而 Authentication#getName 映射到第一個斷言的 NameID 元素。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId 保留與關聯的 RelyingPartyRegistration 的識別碼

自訂 OpenSAML 組態

任何同時使用 Spring Security 和 OpenSAML 的類別都應在類別的開頭靜態初始化 OpenSamlInitializationService

  • Java

  • Kotlin

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

這會取代 OpenSAML 的 InitializationService#initialize

有時,自訂 OpenSAML 如何建置、封送處理和解封送處理 SAML 物件可能很有價值。在這些情況下,您可能反而想要呼叫 OpenSamlInitializationService#requireInitialize(Consumer),這讓您可以存取 OpenSAML 的 XMLObjectProviderFactory

例如,當傳送未簽署的 AuthNRequest 時,您可能想要強制重新驗證。在這種情況下,您可以註冊自己的 AuthnRequestMarshaller,如下所示

  • Java

  • Kotlin

static {
    OpenSamlInitializationService.requireInitialize(factory -> {
        AuthnRequestMarshaller marshaller = new AuthnRequestMarshaller() {
            @Override
            public Element marshall(XMLObject object, Element element) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, element);
            }

            public Element marshall(XMLObject object, Document document) throws MarshallingException {
                configureAuthnRequest((AuthnRequest) object);
                return super.marshall(object, document);
            }

            private void configureAuthnRequest(AuthnRequest authnRequest) {
                authnRequest.setForceAuthn(true);
            }
        }

        factory.getMarshallerFactory().registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller);
    });
}
companion object {
    init {
        OpenSamlInitializationService.requireInitialize {
            val marshaller = object : AuthnRequestMarshaller() {
                override fun marshall(xmlObject: XMLObject, element: Element): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, element)
                }

                override fun marshall(xmlObject: XMLObject, document: Document): Element {
                    configureAuthnRequest(xmlObject as AuthnRequest)
                    return super.marshall(xmlObject, document)
                }

                private fun configureAuthnRequest(authnRequest: AuthnRequest) {
                    authnRequest.isForceAuthn = true
                }
            }
            it.marshallerFactory.registerMarshaller(AuthnRequest.DEFAULT_ELEMENT_NAME, marshaller)
        }
    }
}

requireInitialize 方法每個應用程式實例只能調用一次。

覆寫或取代 Boot 自動組態

Spring Boot 為 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,它代表宣告方和 Relying Party Metadata。這包括諸如 Relying Party 從宣告方請求身份驗證時應使用的 SSO 端點位置之類的事項。

您可以透過發佈自己的 RelyingPartyRegistrationRepository bean 來覆寫預設值。例如,您可以透過點擊宣告方的 Metadata 端點來查找其組態。

Relying Party Registration Repository
  • Java

  • Kotlin

@Value("${metadata.location}")
String assertingPartyMetadataLocation;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
    RelyingPartyRegistration registration = RelyingPartyRegistrations
            .fromMetadataLocation(assertingPartyMetadataLocation)
            .registrationId("example")
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${metadata.location}")
var assertingPartyMetadataLocation: String? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository? {
    val registration = RelyingPartyRegistrations
        .fromMetadataLocation(assertingPartyMetadataLocation)
        .registrationId("example")
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}
registrationId 是您選擇用於區分註冊的任意值。

或者,您可以手動提供每個詳細資訊。

Relying Party Registration Repository 手動組態
  • Java

  • Kotlin

@Value("${verification.key}")
File verificationKey;

@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
    X509Certificate certificate = X509Support.decodeCertificate(this.verificationKey);
    Saml2X509Credential credential = Saml2X509Credential.verification(certificate);
    RelyingPartyRegistration registration = RelyingPartyRegistration
            .withRegistrationId("example")
            .assertingPartyDetails(party -> party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials(c -> c.add(credential))
            )
            .build();
    return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Value("\${verification.key}")
var verificationKey: File? = null

@Bean
open fun relyingPartyRegistrations(): RelyingPartyRegistrationRepository {
    val certificate: X509Certificate? = X509Support.decodeCertificate(verificationKey!!)
    val credential: Saml2X509Credential = Saml2X509Credential.verification(certificate)
    val registration = RelyingPartyRegistration
        .withRegistrationId("example")
        .assertingPartyDetails { party: AssertingPartyDetails.Builder ->
            party
                .entityId("https://idp.example.com/issuer")
                .singleSignOnServiceLocation("https://idp.example.com/SSO.saml2")
                .wantAuthnRequestsSigned(false)
                .verificationX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
                    c.add(
                        credential
                    )
                }
        }
        .build()
    return InMemoryRelyingPartyRegistrationRepository(registration)
}

X509Support 是一個 OpenSAML 類別,在先前的程式碼片段中為了簡潔起見而使用。

或者,您可以使用 DSL 直接連接儲存庫,這也會覆寫自動組態的 SecurityFilterChain

自訂 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()
    }
}

Relying Party 可以是多租戶的,方法是在 RelyingPartyRegistrationRepository 中註冊多個 Relying Party。

RelyingPartyRegistration

RelyingPartyRegistration 實例代表 Relying Party 和宣告方的 Metadata 之間的連結。

RelyingPartyRegistration 中,您可以提供 Relying Party Metadata,例如其 Issuer 值、它預期 SAML 回應傳送到的位置,以及它為了簽署或解密酬載而擁有的任何憑證。

此外,您可以提供宣告方 Metadata,例如其 Issuer 值、它預期 AuthnRequest 傳送到的位置,以及它為了 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。其中一個範例是 Metadata 儲存在資料庫中時。

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

更複雜的設定也是可能的。

  • Java

  • Kotlin

RelyingPartyRegistration relyingPartyRegistration = RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials(c -> c.add(relyingPartyDecryptingCredential()))
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails(party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials(c -> c.add(assertingPartyVerifyingCredential()))
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        )
        .build();
val relyingPartyRegistration =
    RelyingPartyRegistration.withRegistrationId("my-id")
        .entityId("{baseUrl}/{registrationId}")
        .decryptionX509Credentials { c: MutableCollection<Saml2X509Credential?> ->
            c.add(relyingPartyDecryptingCredential())
        }
        .assertionConsumerServiceLocation("/my-login-endpoint/{registrationId}")
        .assertingPartyDetails { party -> party
                .entityId("https://ap.example.org")
                .verificationX509Credentials { c -> c.add(assertingPartyVerifyingCredential()) }
                .singleSignOnServiceLocation("https://ap.example.org/SSO.saml2")
        }
        .build()

最上層的 Metadata 方法是有關 Relying Party 的詳細資訊。assertingPartyDetails 內的方法是有關宣告方的詳細資訊。

Relying Party 預期 SAML 回應的位置是斷言消費者服務位置。

Relying Party 的 entityId 的預設值為 {baseUrl}/saml2/service-provider-metadata/{registrationId}。這是組態宣告方以瞭解您的 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} - 產生 <saml2:AuthnRequest> 的端點,該請求基於該 RelyingPartyRegistration 的組態並將其傳送至宣告方。

  • /login/saml2/sso/ - 驗證宣告方的 <saml2:Response> 的端點;RelyingPartyRegistration 會從先前驗證的狀態或回應的發行者 (如果需要) 查找;也支援 /login/saml2/sso/{registrationId}

  • /logout/saml2/sso - 處理 <saml2:LogoutRequest><saml2:LogoutResponse> 酬載的端點;RelyingPartyRegistration 會從先前驗證的狀態或請求的發行者 (如果需要) 查找;也支援 /logout/saml2/slo/{registrationId}

  • /saml2/metadata - Relying Party Metadata,適用於 RelyingPartyRegistration 的集合;也支援針對特定 RelyingPartyRegistration/saml2/metadata/{registrationId}/saml2/service-provider-metadata/{registrationId}

由於 registrationIdRelyingPartyRegistration 的主要識別碼,因此在未驗證的情況下,URL 中需要它。如果您希望基於任何原因從 URL 中移除 registrationId,您可以指定 RelyingPartyRegistrationResolver,以告知 Spring Security 如何查找 registrationId

憑證

先前顯示的範例中,您可能也注意到使用的憑證。

通常,Relying Party 使用相同的金鑰來簽署酬載以及解密它們。或者,它可以使用相同的金鑰來驗證酬載以及加密它們。

因此,Spring Security 隨附 Saml2X509Credential,這是一種 SAML 專用憑證,可簡化為不同使用案例組態相同金鑰的過程。

至少,您需要擁有來自宣告方的憑證,以便可以驗證宣告方已簽署的回應。

若要建構可用於驗證來自宣告方的斷言的 Saml2X509Credential,您可以載入檔案並使用 CertificateFactory

  • Java

  • Kotlin

Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
    X509Certificate certificate = (X509Certificate)
            CertificateFactory.getInstance("X.509").generateCertificate(is);
    return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
    return Saml2X509Credential.verification(
        CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
    )
}

假設宣告方也將加密斷言。在這種情況下,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 組態

當應用程式使用多個宣告方時,某些組態會在 RelyingPartyRegistration 實例之間重複。

  • Relying Party 的 entityId

  • assertionConsumerServiceLocation

  • 其憑證 — 例如,其簽署或解密憑證

此設定可能會讓某些身份提供者的憑證比其他身份提供者更容易輪換。

可以透過幾種不同的方式減輕重複。

首先,在 YAML 中,可以使用參考來減輕這種情況。

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

其次,在資料庫中,您不需要複製 RelyingPartyRegistration 的模型。

第三,在 Java 中,您可以建立自訂組態方法。

  • Java

  • Kotlin

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

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

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

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

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

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

從請求解析 RelyingPartyRegistration

到目前為止,Spring Security 透過在 URI 路徑中查找註冊 ID 來解析 RelyingPartyRegistration

根據使用案例,也採用了許多其他策略來衍生一個。例如

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

  • 對於處理 <saml2:LogoutRequest>RelyingPartyRegistration 是從目前登入的使用者或從 <saml2:LogoutRequest#Issuer> 元素中查找的。

  • 對於發佈 Metadata,RelyingPartyRegistration 是從也實作 Iterable<RelyingPartyRegistration> 的任何儲存庫中查找的。

當需要調整時,您可以求助於針對自訂此項目的這些端點中的每一個的特定元件。

  • 對於 SAML 回應,自訂 AuthenticationConverter

  • 對於登出請求,自訂 Saml2LogoutRequestValidatorParametersResolver

  • 對於 Metadata,自訂 Saml2MetadataResponseResolver

聯合登入

SAML 2.0 的一種常見安排是具有多個宣告方的身份提供者。在這種情況下,身份提供者的 Metadata 端點會傳回多個 <md:IDPSSODescriptor> 元素。

可以在對 RelyingPartyRegistrations 的單次呼叫中存取這些多個宣告方,如下所示。

  • Java

  • Kotlin

Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map((builder) -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .build()
        )
        .collect(Collectors.toList());
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
        .collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
        .stream().map { builder : RelyingPartyRegistration.Builder -> builder
            .registrationId(UUID.randomUUID().toString())
            .entityId("https://example.org/saml2/sp")
            .assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
            .build()
        }
        .collect(Collectors.toList())

請注意,由於註冊 ID 設定為隨機值,因此這會將某些 SAML 2.0 端點變為不可預測。有多種方法可以解決此問題;讓我們專注於一種適合聯合特定使用案例的方法。

在許多聯合案例中,所有宣告方都共用服務提供者組態。鑑於 Spring Security 預設會將 registrationId 包含在服務提供者 Metadata 中,因此另一個步驟是變更對應的 URI 以排除 registrationId,您可以看到在上述範例中已完成此步驟,其中 entityIdassertionConsumerServiceLocation 組態為靜態端點。

您可以在 我們的 saml-extension-federation 範例中看到已完成的範例。

使用 Spring Security SAML 擴充功能 URI

如果您要從 Spring Security SAML 擴充功能遷移,則組態您的應用程式以使用 SAML 擴充功能 URI 預設值可能會有一些好處。