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