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

首先,使用者對
/private
資源發出未經認證的請求,該請求未被授權。
Spring Security 的
AuthorizationFilter
透過丟擲 AccessDeniedException
表明未經認證的請求被 *拒絕*(Denied)。
由於使用者缺乏授權,
ExceptionTranslationFilter
啟動*開始認證*(Start Authentication)。配置的 AuthenticationEntryPoint
是 LoginUrlAuthenticationEntryPoint
的一個例項,它重定向到生成 <saml2:AuthnRequest>
的端點 Saml2WebSsoAuthenticationRequestFilter
。或者,如果您配置了多個 asserting party,它首先會重定向到一個選擇頁面。
接下來,
Saml2WebSsoAuthenticationRequestFilter
使用其配置的Saml2AuthenticationRequestFactory
建立、簽名、序列化和編碼一個 <saml2:AuthnRequest>
。
然後瀏覽器接收此
<saml2:AuthnRequest>
並將其傳送給 asserting party。asserting party 嘗試認證使用者。如果成功,它會將一個 <saml2:Response>
返回給瀏覽器。
然後瀏覽器將
<saml2:Response>
透過 POST 方法傳送到 assertion consumer service 端點。
下圖展示了 Spring Security 如何認證一個 <saml2:Response>
。

<saml2:Response>
此圖基於我們的 |
當瀏覽器將
<saml2:Response>
提交到應用程式時,它會委託給 Saml2WebSsoAuthenticationFilter
。此過濾器會呼叫其配置的 AuthenticationConverter
,透過從 HttpServletRequest
中提取響應來建立一個 Saml2AuthenticationToken
。此轉換器還會解析 RelyingPartyRegistration
並將其提供給 Saml2AuthenticationToken
。
接下來,過濾器將令牌傳遞給其配置的
AuthenticationManager
。預設情況下,它使用 OpenSamlAuthenticationProvider
。
如果認證失敗,則為*失敗*(Failure)。
-
SecurityContextHolder
會被清除。 -
會呼叫
AuthenticationEntryPoint
以重新啟動認證過程。
如果認證成功,則為*成功*(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
其中
-
idp.example.com/issuer
是 identity provider 發出的 SAML responses 中Issuer
屬性包含的值。 -
classpath:idp.crt
是 identity provider 用於驗證 SAML responses 的證書在 classpath 中的位置。 -
idp.example.com/issuer/sso
是 identity provider 期望接收AuthnRequest
例項的端點。 -
adfs
是您選擇的任意識別符號
就這樣!
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 相對於其他模組來說相當小巧。取而代之的是,諸如 OpenSamlAuthenticationRequestFactory
和 OpenSamlAuthenticationProvider
等類會暴露 Converter
實現,用於定製認證過程中的各個步驟。
例如,一旦您的應用程式接收到 SAMLResponse
並將其委託給 Saml2WebSsoAuthenticationFilter
,該過濾器會將其委託給 OpenSamlAuthenticationProvider
Response
Saml2WebSsoAuthenticationFilter
構建 Saml2AuthenticationToken
並呼叫 AuthenticationManager
。
AuthenticationManager
呼叫 OpenSAML 認證提供者。
認證提供者將響應反序列化為 OpenSAML
Response
並檢查其簽名。如果簽名無效,認證失敗。
然後提供者解密任何
EncryptedAssertion
元素。如果任何解密失敗,認證失敗。
接下來,提供者驗證響應的
Issuer
和 Destination
值。如果它們與 RelyingPartyRegistration
中的值不匹配,認證失敗。
之後,提供者驗證每個
Assertion
的簽名。如果任何簽名無效,認證失敗。此外,如果響應和斷言都沒有簽名,認證也會失敗。響應或所有斷言都必須有簽名。
然後,提供者解密任何
EncryptedID
或 EncryptedAttribute
元素。如果任何解密失敗,認證失敗。
接下來,提供者驗證每個斷言的
ExpiresAt
和 NotBefore
時間戳,<Subject>
和任何 <AudienceRestriction>
條件。如果任何驗證失敗,認證失敗。
之後,提供者獲取第一個斷言的
AttributeStatement
並將其對映到 Map<String, List<Object>>
。它還會授予 ROLE_USER
授權。
最後,它獲取第一個斷言中的
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
看起來像
-
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 來替換它
-
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 的元資料端點來查詢其配置
-
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 {
authorizeRequests {
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
例項代表 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 的詳細資訊。 |
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 的 entityId
和 assertionConsumerServiceLocation
支援以下佔位符
-
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}
由於 registrationId
是 RelyingPartyRegistration
的主要識別符號,在未經認證的場景中,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
,您可以看到在上面的示例中已經這樣做了,其中 entityId
和 assertionConsumerServiceLocation
配置為靜態端點。
您可以在我們的 saml-extension-federation
示例中看到一個完整的示例。
使用 Spring Security SAML 擴充套件 URI
如果您正在從 Spring Security SAML 擴充套件遷移,配置應用程式使用 SAML 擴充套件預設 URI 可能會帶來一些好處。
有關此內容的更多資訊,請參閱我們的 custom-urls
示例和我們的 saml-extension-federation
示例。