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
示例。