SAML 2.0 登入概述
我們首先研究 SAML 2.0 依賴方認證在 Spring Security 中的工作原理。首先,我們看到,與 OAuth 2.0 登入一樣,Spring Security 將使用者帶到第三方進行認證。它透過一系列重定向來實現這一點
首先,使用者向 /private 資源發出未經認證的請求,該請求未被授權。
Spring Security 的 AuthorizationFilter 透過丟擲 AccessDeniedException 來表示未經身份驗證的請求被“拒絕”。
由於使用者缺乏授權,ExceptionTranslationFilter 啟動開始認證。配置的 AuthenticationEntryPoint 是 LoginUrlAuthenticationEntryPoint 的一個例項,它重定向到 <saml2:AuthnRequest> 生成端點 Saml2WebSsoAuthenticationRequestFilter。或者,如果您 配置了多個斷言方,它會首先重定向到一個選擇頁面。
接下來,Saml2WebSsoAuthenticationRequestFilter 使用其配置的 Saml2AuthenticationRequestFactory 建立、簽名、序列化和編碼 <saml2:AuthnRequest>。
然後瀏覽器獲取此 <saml2:AuthnRequest> 並將其呈現給斷言方。斷言方嘗試認證使用者。如果成功,它會返回一個 <saml2:Response> 到瀏覽器。
然後瀏覽器將 <saml2:Response> POST 到斷言消費者服務端點。
下圖顯示了 Spring Security 如何認證 <saml2:Response>。
<saml2:Response>|
該圖基於我們的 |
當瀏覽器嚮應用程式提交 <saml2:Response> 時,它 委託給 Saml2WebSsoAuthenticationFilter。此過濾器呼叫其配置的 AuthenticationConverter,透過從 HttpServletRequest 中提取響應來建立 Saml2AuthenticationToken。此轉換器還會解析 RelyingPartyRegistration 並將其提供給 Saml2AuthenticationToken。
接下來,過濾器將令牌傳遞給其配置的 AuthenticationManager。預設情況下,它使用 OpenSaml5AuthenticationProvider。
如果身份驗證失敗,則為“失敗”。
-
SecurityContextHolder被清除。 -
呼叫
AuthenticationEntryPoint以重新啟動認證過程。
如果身份驗證成功,則為“成功”。
-
Saml2WebSsoAuthenticationFilter呼叫FilterChain#doFilter(request,response)以繼續執行應用程式的其餘邏輯。
最小依賴項
SAML 2.0 服務提供商支援位於 spring-security-saml2-service-provider 中。它基於 OpenSAML 庫構建,因此,您還必須在構建配置中包含 Shibboleth Maven 倉庫。有關為什麼需要單獨倉庫的更多詳細資訊,請檢視 此連結。
-
Maven
-
Gradle
<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 Boot 應用程式中,要指定身份提供商的元資料,請建立類似於以下內容的配置
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
其中:
-
idp.example.com/issuer是身份提供商頒發的 SAML 響應的Issuer屬性中包含的值。 -
classpath:idp.crt是身份提供商用於驗證 SAML 響應的證書在類路徑中的位置。 -
idp.example.com/issuer/sso是身份提供商期望AuthnRequest例項的端點。 -
adfs是 您選擇的任意識別符號
就是這樣!
|
身份提供商和斷言方是同義詞,服務提供商和依賴方也是同義詞。它們分別簡寫為 AP 和 RP。 |
執行時預期
如之前配置的,應用程式處理任何包含 SAMLResponse 引數的 POST /login/saml2/sso/{registrationId} 請求
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
有兩種方法可以誘導斷言方生成 SAMLResponse
-
您可以導航到您的斷言方。它可能有一些連結或按鈕,用於每個註冊的依賴方,您可以單擊這些連結或按鈕來發送
SAMLResponse。 -
您可以導航到應用程式中的受保護頁面 — 例如,
localhost:8080。然後您的應用程式重定向到已配置的斷言方,斷言方再發送SAMLResponse。
從這裡,可以考慮跳轉到
SAML 2.0 登入如何與 OpenSAML 整合
Spring Security 的 SAML 2.0 支援有幾個設計目標
-
依賴庫進行 SAML 2.0 操作和域物件。為此,Spring Security 使用 OpenSAML。
-
確保在使用 Spring Security 的 SAML 支援時不需要此庫。為了實現這一點,Spring Security 在契約中使用 OpenSAML 的任何介面或類都保持封裝。這使得您可以將 OpenSAML 替換為其他庫或不支援的 OpenSAML 版本。
作為這兩個目標的自然結果,Spring Security 的 SAML API 相對於其他模組而言相當小。相反,像 OpenSamlXAuthenticationRequestFactory 和 OpenSamlXAuthenticationProvider 這樣的類公開了 Converter 實現,這些實現可以自定義認證過程中的各個步驟。
例如,一旦您的應用程式收到 SAMLResponse 並委託給 Saml2WebSsoAuthenticationFilter,該過濾器會委託給 OpenSamlXAuthenticationProvider
Response
Saml2WebSsoAuthenticationFilter 組織 Saml2AuthenticationToken 並呼叫 AuthenticationManager。
AuthenticationManager 呼叫 OpenSAML 認證提供商。
認證提供商將響應反序列化為 OpenSAML Response 並檢查其簽名。如果簽名無效,認證失敗。
然後提供商解密任何 EncryptedAssertion 元素。如果任何解密失敗,認證失敗。
接下來,提供商驗證響應的 Issuer 和 Destination 值。如果它們與 RelyingPartyRegistration 中的不匹配,認證失敗。
之後,提供商驗證每個 Assertion 的簽名。如果任何簽名無效,認證失敗。此外,如果響應和斷言都沒有簽名,認證失敗。響應或所有斷言都必須有簽名。
然後,提供商解密任何 EncryptedID 或 EncryptedAttribute 元素。如果任何解密失敗,認證失敗。
接下來,提供商驗證每個斷言的 ExpiresAt 和 NotBefore 時間戳、<Subject> 和任何 <AudienceRestriction> 條件。如果任何驗證失敗,認證失敗。
緊接著,提供商獲取第一個斷言的 AttributeStatement 並將其對映到 Map<String, List<Object>>。它還授予 FACTOR_SAML_RESPONSE 和 ROLE_USER 授權。
最後,它從第一個斷言中獲取 NameID,屬性的 Map 和 GrantedAuthority,並構建一個 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 為依賴方生成兩個 @Bean 物件。
第一個是 SecurityFilterChain,它將應用程式配置為依賴方。當包含 spring-security-saml2-service-provider 時,SecurityFilterChain 看起來像
-
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 {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
saml2Login { }
}
return http.build()
}
如果應用程式未公開 SecurityFilterChain bean,Spring Boot 會公開上述預設 bean。
您可以透過在應用程式中公開 bean 來替換它
-
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 {
authorizeHttpRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
}
}
return http.build()
}
}
上述示例要求任何以 /messages/ 開頭的 URL 具有 USER 角色。
Spring Boot 建立的第二個 @Bean 是 RelyingPartyRegistrationRepository,它代表斷言方和依賴方元資料。這包括依賴方在請求斷言方認證時應使用的 SSO 端點位置等資訊。
您可以透過釋出自己的 RelyingPartyRegistrationRepository bean 來覆蓋預設設定。例如,您可以透過訪問斷言方的元資料端點來查詢其配置
-
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 是您選擇的用於區分註冊的任意值。 |
或者,您可以手動提供每個詳細資訊
-
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)
}
|
|
或者,您可以使用 DSL 直接連線倉庫,這也會覆蓋自動配置的 SecurityFilterChain
-
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 {
authorizeHttpRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
relyingPartyRegistrationRepository = relyingPartyRegistrations()
}
}
return http.build()
}
}
|
依賴方可以透過在 |
如果您希望元資料定期重新整理,您可以將倉庫包裝在 CachingRelyingPartyRegistrationRepository 中,如下所示
-
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 例項表示依賴方和斷言方元資料之間的連結。
在 RelyingPartyRegistration 中,您可以提供依賴方元資料,例如其 Issuer 值,它期望 SAML 響應傳送到的位置,以及它用於簽名或解密有效載荷的任何憑據。
此外,您可以提供斷言方元資料,例如其 Issuer 值,它期望 AuthnRequest 傳送到的位置,以及它用於依賴方驗證或加密有效載荷的任何公共憑據。
以下 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()
|
頂層元資料方法是關於依賴方的詳細資訊。 |
|
依賴方期望 SAML 響應的位置是斷言消費者服務位置。 |
依賴方 entityId 的預設值為 {baseUrl}/saml2/service-provider-metadata/{registrationId}。這是配置斷言方以瞭解您的依賴方所需的值。
assertionConsumerServiceLocation 的預設值為 /login/saml2/sso/{registrationId}。預設情況下,它對映到過濾器鏈中的 Saml2WebSsoAuthenticationFilter。
URI 模式
您可能已經注意到前面示例中的 {baseUrl} 和 {registrationId} 佔位符。
這些對於生成 URI 非常有用。因此,依賴方的 entityId 和 assertionConsumerServiceLocation 支援以下佔位符
-
baseUrl- 已部署應用程式的方案、主機和埠 -
registrationId- 此依賴方的註冊 ID -
baseScheme- 已部署應用程式的方案 -
baseHost- 已部署應用程式的主機 -
basePort- 已部署應用程式的埠
例如,前面定義的 assertionConsumerServiceLocation 是
/my-login-endpoint/{registrationId}
在部署的應用程式中,它轉換為
/my-login-endpoint/adfs
前面顯示的 entityId 定義為
{baseUrl}/{registrationId}
在部署的應用程式中,它轉換為
https://rp.example.com/adfs
主要的 URI 模式如下
-
/saml2/authenticate/{registrationId}- 根據該RelyingPartyRegistration的配置生成<saml2:AuthnRequest>並將其傳送到斷言方的端點 -
/login/saml2/sso/- 認證斷言方<saml2:Response>的端點;如果需要,RelyingPartyRegistration從先前認證的狀態或響應的頒發者中查詢;也支援/login/saml2/sso/{registrationId} -
/logout/saml2/sso- 處理<saml2:LogoutRequest>和<saml2:LogoutResponse>有效載荷 的端點;如果需要,RelyingPartyRegistration從當前登入使用者或請求的頒發者中查詢;也支援/logout/saml2/slo/{registrationId} -
/saml2/metadata- 依賴方元資料,用於RelyingPartyRegistration的集合;也支援/saml2/metadata/{registrationId}或/saml2/service-provider-metadata/{registrationId}用於特定的RelyingPartyRegistration
由於 registrationId 是 RelyingPartyRegistration 的主要識別符號,因此在未認證場景中,URL 中需要它。如果您希望出於任何原因從 URL 中刪除 registrationId,您可以 指定 RelyingPartyRegistrationResolver 來告訴 Spring Security 如何查詢 registrationId。
憑據
在前面的示例中,您可能還注意到了使用的憑據。
通常,依賴方使用相同的金鑰來簽名有效載荷並解密它們。或者,它可以使用相同的金鑰來驗證有效載荷並加密它們。
因此,Spring Security 提供了 Saml2X509Credential,這是一種特定於 SAML 的憑據,它簡化了為不同用例配置相同金鑰的過程。
至少,您需要斷言方的證書,以便驗證斷言方簽名的響應。
要構造一個可用於驗證斷言方斷言的 Saml2X509Credential,您可以載入檔案並使用 CertificateFactory
-
Java
-
Kotlin
Resource resource = new ClassPathResource("ap.crt");
try (InputStream is = resource.getInputStream()) {
X509Certificate certificate = (X509Certificate)
CertificateFactory.getInstance("X.509").generateCertificate(is);
return Saml2X509Credential.verification(certificate);
}
val resource = ClassPathResource("ap.crt")
resource.inputStream.use {
return Saml2X509Credential.verification(
CertificateFactory.getInstance("X.509").generateCertificate(it) as X509Certificate?
)
}
假設斷言方還將加密斷言。在這種情況下,依賴方需要私鑰來解密加密值。
在這種情況下,您需要一個 RSAPrivateKey 及其對應的 X509Certificate。您可以使用 Spring Security 的 RsaKeyConverters 實用類載入第一個,並像之前一樣載入第二個
-
Java
-
Kotlin
X509Certificate certificate = relyingPartyDecryptionCertificate();
Resource resource = new ClassPathResource("rp.crt");
try (InputStream is = resource.getInputStream()) {
RSAPrivateKey rsa = RsaKeyConverters.pkcs8().convert(is);
return Saml2X509Credential.decryption(rsa, certificate);
}
val certificate: X509Certificate = relyingPartyDecryptionCertificate()
val resource = ClassPathResource("rp.crt")
resource.inputStream.use {
val rsa: RSAPrivateKey = RsaKeyConverters.pkcs8().convert(it)
return Saml2X509Credential.decryption(rsa, certificate)
}
|
當您將這些檔案的位置指定為相應的 Spring Boot 屬性時,Spring Boot 會為您執行這些轉換。 |
重複的依賴方配置
當應用程式使用多個斷言方時,某些配置會在 RelyingPartyRegistration 例項之間重複
-
依賴方的
entityId -
其
assertionConsumerServiceLocation -
其憑據 — 例如,其簽名或解密憑據
這種設定可能使得某些身份提供商的憑據比其他身份提供商更容易輪換。
可以通過幾種不同的方式來緩解重複。
首先,在 YAML 中可以透過引用來緩解
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 的模型。
第三,在 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>元素中查詢 -
對於釋出元資料,
RelyingPartyRegistration從任何也實現Iterable<RelyingPartyRegistration>的倉庫中查詢
當這需要調整時,您可以轉向每個端點的特定元件,這些元件旨在自定義此功能
-
對於 SAML 響應,自定義
AuthenticationConverter -
對於登出請求,自定義
Saml2LogoutRequestValidatorParametersResolver -
對於元資料,自定義
Saml2MetadataResponseResolver
聯合登入
SAML 2.0 的一種常見安排是具有多個斷言方的身份提供商。在這種情況下,身份提供商的元資料端點返回多個 <md:IDPSSODescriptor> 元素。
可以透過一次呼叫 RelyingPartyRegistrations 來訪問這些多個斷言方,如下所示
-
Java
-
Kotlin
Collection<RelyingPartyRegistration> registrations = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map((builder) -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.build()
)
.collect(Collectors.toList());
var registrations: Collection<RelyingPartyRegistration> = RelyingPartyRegistrations
.collectionFromMetadataLocation("https://example.org/saml2/idp/metadata.xml")
.stream().map { builder : RelyingPartyRegistration.Builder -> builder
.registrationId(UUID.randomUUID().toString())
.entityId("https://example.org/saml2/sp")
.assertionConsumerServiceLocation("{baseUrl}/login/saml2/sso")
.build()
}
.collect(Collectors.toList())
請注意,由於註冊 ID 設定為隨機值,這將導致某些 SAML 2.0 端點變得不可預測。有幾種方法可以解決這個問題;讓我們關注一種適用於聯邦特定用例的方法。
在許多聯邦案例中,所有斷言方共享服務提供商配置。鑑於 Spring Security 預設會在服務提供商元資料中包含 registrationId,因此另一個步驟是更改相應的 URI 以排除 registrationId,您可以看到在上面的示例中已經完成了此操作,其中 entityId 和 assertionConsumerServiceLocation 配置了一個靜態端點。
您可以在我們的 saml-extension-federation 示例中看到一個完整的示例。
使用 Spring Security SAML 擴充套件 URI
如果您正在從 Spring Security SAML 擴充套件遷移,將應用程式配置為使用 SAML 擴充套件 URI 預設值可能會有一些好處。
有關此內容的更多資訊,請參閱我們的 custom-urls 示例和我們的 saml-extension-federation 示例。