|
此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.3! |
SAML 2.0 登录概述
让我们看一下 SAML 2.0 依赖方身份验证在 Spring Security 中是如何工作的。 首先,我们看到,与 OAuth 2.0 登录一样, Spring Security 将用户带到第三方进行身份验证。 它通过一系列重定向来实现这一点。
首先,用户向资源发出未经身份验证的请求/private它未获得授权。
Spring Security 的FilterSecurityInterceptor表示未经身份验证的请求被拒绝,方法是抛出AccessDeniedException.
由于用户没有授权,因此ExceptionTranslationFilter启动 Start Authentication。
配置的AuthenticationEntryPoint是LoginUrlAuthenticationEntryPoint重定向到这<saml2:AuthnRequest>生成终端节点,Saml2WebSsoAuthenticationRequestFilter.
或者,如果您配置了多个断言方,它将首先重定向到选取器页面。
接下来,Saml2WebSsoAuthenticationRequestFilter创建、签署、序列化和编码<saml2:AuthnRequest>使用其配置的Saml2AuthenticationRequestFactory.
然后,浏览器会采用<saml2:AuthnRequest>并将其提交给主张方。
断言方尝试对用户进行身份验证。
如果成功,它将返回一个<saml2:Response>返回浏览器。
然后,浏览器将<saml2:Response>添加到断言使用者服务终端节点。
<saml2:Response>该图建立在我们的SecurityFilterChain图。
当浏览器提交<saml2:Response>对于应用程序,它delegates 到Saml2WebSsoAuthenticationFilter.
此过滤器将其配置的AuthenticationConverter要创建一个Saml2AuthenticationToken通过从HttpServletRequest.
此转换器还解决了RelyingPartyRegistration并将其提供给Saml2AuthenticationToken.
接下来,过滤器将令牌传递给其配置的AuthenticationManager.
默认情况下,它将使用OpenSAML authentication provider.
如果身份验证失败,则失败
-
这
AuthenticationEntryPoint以重新启动身份验证过程。
如果身份验证成功,则为 Success。
-
这
Saml2WebSsoAuthenticationFilter调用FilterChain#doFilter(request,response)以继续执行 Application Logic 的其余部分。
最小依赖项
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 时,将应用程序配置为服务提供商包括两个基本步骤。 首先,包括所需的依赖项,其次,指示必要的断言方元数据。
| 此外,这假定您已经向断言方注册了依赖方。 |
指定身份提供程序元数据
在 Spring Boot 应用程序中,要指定身份提供者的元数据,只需执行以下作:
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是Issuer身份提供商将发出的 SAML 响应的属性 -
classpath:idp.crt是身份提供者证书在类路径上用于验证 SAML 响应的位置,以及 -
idp.example.com/issuer/sso是身份提供商期望的终端节点AuthnRequests. -
adfs是您选择的任意标识符
就是这样!
| 身份提供商和断言方是同义词,服务提供商和依赖方也是同义词。 它们通常分别缩写为 AP 和 RP。 |
运行时预期
如上配置,应用程序会处理任何POST /login/saml2/sso/{registrationId}请求中包含SAMLResponse参数:
POST /login/saml2/sso/adfs HTTP/1.1
SAMLResponse=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZ...
有两种方法可以查看 induce your asserting party 生成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 在 Contract 中使用 OpenSAML 的任何接口或类都保持封装状态。 这使您可以将 OpenSAML 切换为其他库,甚至是不受支持的 OpenSAML 版本。
作为上述两个目标的自然结果, Spring Security 的 SAML API 相对于其他模块来说相当小。
相反,像OpenSaml4AuthenticationRequestFactory和OpenSaml4AuthenticationProvider暴露Converter自定义身份验证过程中的各个步骤。
例如,一旦您的应用程序收到SAMLResponse和委托人Saml2WebSsoAuthenticationFilter,过滤器将委托给OpenSaml4AuthenticationProvider.
| 为了向后兼容,Spring Security 将默认使用最新的 OpenSAML 3。 请注意,尽管 OpenSAML 3 已达到其生命周期结束,建议更新到 OpenSAML 4.x。 因此,Spring Security 同时支持 OpenSAML 3.x 和 4.x。 如果您将 OpenSAML 依赖项管理到 4.x,则 Spring Security 将选择其 OpenSAML 4.x 实现。 |
Response
这Saml2WebSsoAuthenticationFilter制定Saml2AuthenticationToken并调用AuthenticationManager.
这AuthenticationManager调用 OpenSAML 身份验证提供程序。
身份验证提供程序将响应反序列化为 OpenSAMLResponse并检查其签名。
如果签名无效,则鉴权失败。
然后,提供程序解密任何EncryptedAssertion元素.
如果任何解密失败,则身份验证将失败。
接下来,提供程序验证响应的Issuer和Destination值。
如果它们与RelyingPartyRegistration,则身份验证失败。
之后,提供程序会验证每个Assertion.
如果签名无效,则鉴权失败。
此外,如果响应和断言都没有签名,则身份验证将失败。
响应或所有断言都必须具有签名。
然后,提供程序 ,解密任何EncryptedID或EncryptedAttribute元素]。
如果任何解密失败,则身份验证将失败。
接下来,提供程序验证每个断言的ExpiresAt和NotBeforetimestamps、<Subject>和任何<AudienceRestriction>条件。
如果任何验证失败,则身份验证失败。
然后,提供程序采用第一个断言的AttributeStatement并将其映射到Map<String, List<Object>>.
它还授予ROLE_USER授予权限。
最后,它需要NameID从第一个断言开始,Mapof 属性和GrantedAuthority并构造一个Saml2AuthenticatedPrincipal.
然后,它将该 principal 和权限放入Saml2Authentication.
结果Authentication#getPrincipal是 Spring SecuritySaml2AuthenticatedPrincipalobject 和Authentication#getName映射到第一个断言的NameID元素。Saml2AuthenticatedPrincipal#getRelyingPartyRegistrationId持有identifier 添加到关联的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)
}
}
}
这requireInitializemethod 每个应用程序实例只能调用一次。
覆盖或替换引导自动配置
有两个@Bean的 Spring Boot 为依赖方生成的。
第一个是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 {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login { }
}
return http.build()
}
如果应用程序没有公开SecurityFilterChainbean,则 Spring Boot 将公开上述默认的 bean。
您可以通过在应用程序中公开 bean 来替换它:
-
Java
-
Kotlin
@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();
}
}
@EnableWebSecurity
class MyCustomSecurityConfiguration {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeRequests {
authorize("/messages/**", hasAuthority("ROLE_USER"))
authorize(anyRequest, authenticated)
}
saml2Login {
}
}
return http.build()
}
}
以上需要USER对于任何以/messages/.
第二个@BeanSpring Boot 创建的是一个RelyingPartyRegistrationRepository,它表示断言方和依赖方元数据。
这包括依赖方在向断言方请求身份验证时应使用的 SSO 端点的位置等信息。
您可以通过发布自己的RelyingPartyRegistrationRepository豆。
例如,您可以通过点击其元数据端点来查找断言方的配置,如下所示:
-
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")
.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:
-
Java
-
Kotlin
@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();
}
}
@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()
}
}
通过在RelyingPartyRegistrationRepository. |
RelyingParty注册
一个RelyingPartyRegistrationinstance 表示依赖方和评估方的元数据之间的链接。
在RelyingPartyRegistration,您可以提供依赖方元数据,例如其Issuer值,它希望将 SAML 响应发送到其中,以及它拥有的用于对负载进行签名或解密的任何凭证。
此外,您还可以提供断言参与方元数据,例如其Issuer值,它希望将 AuthnRequests 发送到其中,以及它拥有的任何公有凭证,以便依赖方验证或加密负载。
以下内容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()
请注意,您还可以创建一个RelyingPartyRegistration从任意InputStream源。
一个这样的例子是当元数据存储在数据库中时:
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()
顶级元数据方法是有关信赖方的详细信息。
里面的方法assertingPartyDetails是有关断言方的详细信息。 |
| 依赖方需要 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}- 端点生成一个<saml2:AuthnRequest>基于该RelyingPartyRegistration并将其发送给断言方 -
/saml2/login/sso/{registrationId}- 端点验证断言方的<saml2:Response>基于该RelyingPartyRegistration -
/saml2/logout/sso- 端点过程<saml2:LogoutRequest>和<saml2:LogoutResponse>负载;这RelyingPartyRegistration从先前的身份验证状态中查找 -
/saml2/saml2-service-provider/metadata/{registrationId}- 该RelyingPartyRegistration
由于registrationId是RelyingPartyRegistration,则对于未经身份验证的方案,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 的RsaKeyConvertersUtility 类和第二个类一样:
-
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:
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 解析了RelyingPartyRegistration通过在 URI 路径中查找注册 ID。
您可能想要自定义它的原因有很多。其中包括:
要自定义RelyingPartyRegistration已解决,您可以配置自定义RelyingPartyRegistrationResolver.
默认从 URI 的最后一个 path 元素中查找注册 ID,并在RelyingPartyRegistrationRepository.
请记住,如果您的RelyingPartyRegistration,则您的 Resolver 实现应该可以解决它们。 |
解析为单个 consistentRelyingPartyRegistration
例如,您可以提供一个解析程序,该解析程序始终返回相同的RelyingPartyRegistration:
-
Java
-
Kotlin
public class SingleRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final RelyingPartyRegistrationResolver delegate;
public SingleRelyingPartyRegistrationResolver(RelyingPartyRegistrationRepository registrations) {
this.delegate = new DefaultRelyingPartyRegistrationResolver(registrations);
}
@Override
public RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
return this.delegate.resolve(request, "single");
}
}
class SingleRelyingPartyRegistrationResolver(delegate: RelyingPartyRegistrationResolver) : RelyingPartyRegistrationResolver {
override fun resolve(request: HttpServletRequest?, registrationId: String?): RelyingPartyRegistration? {
return this.delegate.resolve(request, "single")
}
}
接下来,您可能会了解如何使用此解析程序进行自定义<saml2:SPSSODescriptor>元数据生产. |
基于<saml2:Response#Issuer>
当一个依赖方可以接受来自多个断言方的断言时,您将拥有同样多的断言RelyingPartyRegistration作为断言方,并且依赖方信息在每个实例中重复。
这意味着每个断言方的断言使用者服务端点都不同,这可能并不可取。
您可以改为解析registrationId通过Issuer.
的自定义实现RelyingPartyRegistrationResolver这可能看起来像:
-
Java
-
Kotlin
public class SamlResponseIssuerRelyingPartyRegistrationResolver implements RelyingPartyRegistrationResolver {
private final InMemoryRelyingPartyRegistrationRepository registrations;
// ... constructor
@Override
RelyingPartyRegistration resolve(HttpServletRequest request, String registrationId) {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId);
}
String entityId = resolveEntityIdFromSamlResponse(request);
for (RelyingPartyRegistration registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration;
}
}
return null;
}
private String resolveEntityIdFromSamlResponse(HttpServletRequest request) {
// ...
}
}
class SamlResponseIssuerRelyingPartyRegistrationResolver(val registrations: InMemoryRelyingPartyRegistrationRepository):
RelyingPartyRegistrationResolver {
@Override
fun resolve(val request: HttpServletRequest, val registrationId: String): RelyingPartyRegistration {
if (registrationId != null) {
return this.registrations.findByRegistrationId(registrationId)
}
String entityId = resolveEntityIdFromSamlResponse(request)
for (val registration : this.registrations) {
if (registration.getAssertingPartyDetails().getEntityId().equals(entityId)) {
return registration
}
}
return null
}
private resolveEntityIdFromSamlResponse(val request: HttpServletRequest): String {
// ...
}
}
接下来,您可能会了解如何使用此解析程序进行自定义<saml2:Response>认证. |
联合登录
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")
.build()
}
.collect(Collectors.toList()));
请注意,由于注册 ID 设置为随机值,因此这会将某些 SAML 2.0 端点更改为不可预测。 有几种方法可以解决这个问题;让我们专注于一种适合联合特定用例的方法。
在许多联合身份验证情况下,所有断言方共享服务提供商配置。
鉴于 Spring Security 默认包含registrationId在其所有许多 SAML 2.0 URI 中,下一步通常是更改这些 URI 以排除registrationId.
您需要按照这些思路更改两个主要的 URI:
| (可选)您可能还希望更改 Authentication Request 位置,但由于这是应用程序内部的 URI,而不是发布给断言方,因此好处通常很小。 |
使用 Spring Security SAML 扩展 URI
如果要从 Spring Security SAML 扩展迁移,则将应用程序配置为使用 SAML 扩展 URI 默认值可能会有一些好处。
有关更多信息,请参阅我们custom-urls样本和我们saml-extension-federation样本.