执行单一注销
除了其他注销机制之外,Spring Security 还支持由 RP 和 AP 发起的 SAML 2.0 单一注销。
简而言之,Spring Security 支持两种用例
-
RP 发起 - 您的应用程序有一个端点,当 POST 到该端点时,将注销用户并向断言方发送一个
saml2:LogoutRequest。此后,断言方将返回一个saml2:LogoutResponse并允许您的应用程序响应。 -
AP 发起 - 您的应用程序有一个端点,该端点将接收来自断言方的
saml2:LogoutRequest。您的应用程序将在此点完成其注销,然后向断言方发送一个saml2:LogoutResponse。
在 AP 发起 场景中,您的应用程序在注销后可能执行的任何本地重定向都将失效。一旦您的应用程序发送 saml2:LogoutResponse,它就不再控制浏览器。 |
单一注销的最小配置
要使用 Spring Security 的 SAML 2.0 单一注销功能,您需要以下内容
-
首先,断言方必须支持 SAML 2.0 单一注销。
-
其次,断言方应配置为对您的应用程序的
/logout/saml2/slo端点进行签名和 POSTsaml2:LogoutRequest和saml2:LogoutResponse。 -
第三,您的应用程序必须有一个 PKCS#8 私钥和 X.509 证书,用于签名
saml2:LogoutRequest和saml2:LogoutResponse。
您可以通过以下方式在 Spring Boot 中实现这一点
spring:
security:
saml2:
relyingparty:
registration:
metadata:
signing.credentials: (3)
- private-key-location: classpath:credentials/rp-private.key
certificate-location: classpath:credentials/rp-certificate.crt
singlelogout.url: "{baseUrl}/logout/saml2/slo" (2)
assertingparty:
metadata-uri: https://ap.example.com/metadata (1)
| 1 | - IDP 的元数据 URI,这将向您的应用程序指示其对 SLO 的支持。 |
| 2 | - 您的应用程序中的 SLO 端点。 |
| 3 | - 用于签名 <saml2:LogoutRequest> 和 <saml2:LogoutResponse> 的签名凭据。 |
An asserting party supports Single Logout if their metadata includes the `<SingleLogoutService>` element in their metadata.
就是这样!
Spring Security 的注销支持提供了许多配置点。考虑以下用例
启动预期
当使用这些属性时,除了登录之外,SAML 2.0 服务提供商将自动配置自身,以通过 RP 或 AP 发起的注销来促进 <saml2:LogoutRequest> 和 <saml2:LogoutResponse> 的注销。
它通过确定性启动过程实现这一点
-
查询身份服务器元数据端点以获取
<SingleLogoutService>元素。 -
扫描元数据并缓存任何公共签名验证密钥。
-
准备适当的端点。
此过程的一个结果是身份服务器必须启动并接收请求,以便服务提供商成功启动。
| 如果服务提供商查询身份服务器时身份服务器已关闭(在适当的超时情况下),则启动将失败。 |
运行时预期
给定上述配置,任何已登录用户都可以向您的应用程序发送 POST /logout 以执行 RP 发起的 SLO。您的应用程序将执行以下操作:
-
注销用户并使会话失效。
-
生成
<saml2:LogoutRequest>并将其 POST 到关联的断言方的 SLO 端点。 -
然后,如果断言方响应
<saml2:LogoutResponse>,应用程序将对其进行验证并重定向到配置的成功端点。
此外,当断言方将 <saml2:LogoutRequest> 发送到 /logout/saml2/slo 时,您的应用程序可以参与 AP 发起的注销。发生这种情况时,您的应用程序将执行以下操作:
-
验证
<saml2:LogoutRequest>。 -
注销用户并使会话失效。
-
生成
<saml2:LogoutResponse>并将其 POST 回断言方的 SLO 端点。
没有 Boot 的最小配置
您也可以通过直接发布 bean 来实现相同的结果,而不是使用 Boot 属性,如下所示:
-
Java
-
Kotlin
@Configuration
public class SecurityConfig {
@Value("${private.key}") RSAPrivateKey key;
@Value("${public.certificate}") X509Certificate certificate;
@Bean
RelyingPartyRegistrationRepository registrations() {
Saml2X509Credential credential = Saml2X509Credential.signing(key, certificate);
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata") (1)
.registrationId("metadata")
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") (2)
.signingX509Credentials((signing) -> signing.add(credential)) (3)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.saml2Login(withDefaults())
.saml2Logout(withDefaults()); (4)
return http.build();
}
}
@Configuration
class SecurityConfig(@Value("${private.key}") val key: RSAPrivateKey,
@Value("${public.certificate}") val certificate: X509Certificate) {
@Bean
fun registrations(): RelyingPartyRegistrationRepository {
val credential = Saml2X509Credential.signing(key, certificate)
val registration = RelyingPartyRegistrations
.fromMetadataLocation("https://ap.example.org/metadata") (1)
.registrationId("metadata")
.singleLogoutServiceLocation("{baseUrl}/logout/saml2/slo") (2)
.signingX509Credentials({ signing: List<Saml2X509Credential> -> signing.add(credential) }) (3)
.build()
return InMemoryRelyingPartyRegistrationRepository(registration)
}
@Bean
fun web(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
anyRequest = authenticated
}
saml2Login {
}
saml2Logout { (4)
}
}
return http.build()
}
}
| 1 | - IDP 的元数据 URI,这将向您的应用程序指示其对 SLO 的支持。 |
| 2 | - 您的应用程序中的 SLO 端点。 |
| 3 | - 用于签名 <saml2:LogoutRequest> 和 <saml2:LogoutResponse> 的签名凭据,您也可以将其添加到多个信赖方。 |
| 4 | - 其次,指示您的应用程序希望使用 SAML SLO 来注销最终用户。 |
添加 saml2Logout 为您的整个服务提供商添加了注销功能。由于它是一个可选功能,您需要为每个单独的 RelyingPartyRegistration 启用它。您可以通过设置 RelyingPartyRegistration.Builder#singleLogoutServiceLocation 属性(如上所示)来完成此操作。 |
SAML 2.0 注销的工作原理
接下来,让我们看看 Spring Security 用于支持我们刚刚看到的基于 servlet 的应用程序中的 SAML 2.0 注销的架构组件。
对于 RP 发起的注销
Spring Security 执行其注销流程,调用其 LogoutHandler 以使会话失效并执行其他清理。然后,它调用 Saml2RelyingPartyInitiatedLogoutSuccessHandler。
注销成功处理程序使用 Saml2LogoutRequestResolver 实例来创建、签名和序列化 <saml2:LogoutRequest>。它使用与当前 Saml2AuthenticatedPrincipal 相关联的 RelyingPartyRegistration 中的密钥和配置。然后,它通过重定向 POST 将 <saml2:LogoutRequest> 发送到断言方 SLO 端点。
浏览器将控制权交给断言方。如果断言方重定向回来(可能不会),则应用程序继续执行步骤
。
Saml2LogoutResponseFilter 使用其 Saml2LogoutResponseValidator 反序列化、验证和处理 <saml2:LogoutResponse>。
如果有效,则通过重定向到 /login?logout 或已配置的任何内容来完成本地注销流程。如果无效,则响应 400。
对于 AP 发起的注销
Saml2LogoutRequestFilter 使用其 Saml2LogoutRequestValidator 反序列化、验证和处理 <saml2:LogoutRequest>。
如果有效,则过滤器调用已配置的 LogoutHandler,使会话失效并执行其他清理。
它使用 Saml2LogoutResponseResolver 来创建、签名和序列化 <saml2:LogoutResponse>。它使用从端点或从 <saml2:LogoutRequest> 的内容派生的 RelyingPartyRegistration 中的密钥和配置。然后,它通过重定向 POST 将 <saml2:LogoutResponse> 发送到断言方 SLO 端点。
浏览器将控制权交给断言方。
如果无效,则它响应 400。
配置注销端点
有三种行为可以通过不同的端点触发
-
RP 发起的注销,它允许已认证的用户
POST并通过向断言方发送<saml2:LogoutRequest>来触发注销过程。 -
AP 发起的注销,它允许断言方向应用程序发送
<saml2:LogoutRequest>。 -
AP 注销响应,它允许断言方响应 RP 发起的
<saml2:LogoutRequest>而发送<saml2:LogoutResponse>。
第一个由主体类型为 Saml2AuthenticatedPrincipal 时执行正常的 POST /logout 触发。
第二个由向 /logout/saml2/slo 端点 POST 带有断言方签名的 SAMLRequest 触发。
第三个由向 /logout/saml2/slo 端点 POST 带有断言方签名的 SAMLResponse 触发。
由于用户已登录或已知原始注销请求,因此 registrationId 已知。因此,{registrationId} 默认不作为这些 URL 的一部分。
此 URL 可以在 DSL 中自定义。
例如,如果您正在将现有的信赖方迁移到 Spring Security,您的断言方可能已经指向 GET /SLOService.saml2。为了减少断言方的配置更改,您可以在 DSL 中配置过滤器,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
);
http {
saml2Logout {
logoutRequest {
logoutUrl = "/SLOService.saml2"
}
logoutResponse {
logoutUrl = "/SLOService.saml2"
}
}
}
您还应该在 RelyingPartyRegistration 中配置这些端点。
此外,您可以像这样自定义用于在本地触发注销的端点:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));
http {
saml2Logout {
logoutUrl = "/saml2/logout"
}
}
将本地注销与 SAML 2.0 注销分离
在某些情况下,您可能希望为一个本地注销端点和一个 RP 发起的 SLO 端点公开一个注销端点。与其他注销机制一样,您可以注册多个,只要它们各自具有不同的端点。
因此,例如,您可以像这样连接 DSL
-
Java
-
Kotlin
http
.logout((logout) -> logout.logoutUrl("/logout"))
.saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));
http {
logout {
logoutUrl = "/logout"
}
saml2Logout {
logoutUrl = "/saml2/logout"
}
}
现在,如果客户端发送 POST /logout,会话将被清除,但不会向断言方发送 <saml2:LogoutRequest>。但是,如果客户端发送 POST /saml2/logout,则应用程序将正常启动 SAML 2.0 SLO。
自定义 <saml2:LogoutRequest> 解析
通常需要设置 <saml2:LogoutRequest> 中的其他值,而不是 Spring Security 提供的默认值。
默认情况下,Spring Security 将发出 <saml2:LogoutRequest> 并提供
-
DestinationValidator属性 - 来自RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation -
ID属性 - 一个 GUID -
<Issuer>元素 - 来自RelyingPartyRegistration#getEntityId -
<NameID>元素 - 来自Authentication#getName
要添加其他值,您可以使用委托,如下所示:
-
Java
-
Kotlin
@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
OpenSaml5LogoutRequestResolver logoutRequestResolver =
new OpenSaml5LogoutRequestResolver(registrations);
logoutRequestResolver.setParametersConsumer((parameters) -> {
String name = ((Saml2AuthenticatedPrincipal) parameters.getAuthentication().getPrincipal()).getFirstAttribute("CustomAttribute");
String format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
LogoutRequest logoutRequest = parameters.getLogoutRequest();
NameID nameId = logoutRequest.getNameID();
nameId.setValue(name);
nameId.setFormat(format);
});
return logoutRequestResolver;
}
@Bean
open fun logoutRequestResolver(registrations:RelyingPartyRegistrationRepository?): Saml2LogoutRequestResolver {
val logoutRequestResolver = OpenSaml5LogoutRequestResolver(registrations)
logoutRequestResolver.setParametersConsumer { parameters: LogoutRequestParameters ->
val name: String = (parameters.getAuthentication().getPrincipal() as Saml2AuthenticatedPrincipal).getFirstAttribute("CustomAttribute")
val format = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
val logoutRequest: LogoutRequest = parameters.getLogoutRequest()
val nameId: NameID = logoutRequest.getNameID()
nameId.setValue(name)
nameId.setFormat(format)
}
return logoutRequestResolver
}
然后,您可以在 DSL 中提供您的自定义 Saml2LogoutRequestResolver,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestResolver = this.logoutRequestResolver
}
}
}
自定义 <saml2:LogoutResponse> 解析
通常需要设置 <saml2:LogoutResponse> 中的其他值,而不是 Spring Security 提供的默认值。
默认情况下,Spring Security 将发出 <saml2:LogoutResponse> 并提供
-
DestinationValidator属性 - 来自RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation -
ID属性 - 一个 GUID -
<Issuer>元素 - 来自RelyingPartyRegistration#getEntityId -
<Status>元素 -SUCCESS
要添加其他值,您可以使用委托,如下所示:
-
Java
-
Kotlin
@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
OpenSaml5LogoutResponseResolver logoutRequestResolver =
new OpenSaml5LogoutResponseResolver(registrations);
logoutRequestResolver.setParametersConsumer((parameters) -> {
if (checkOtherPrevailingConditions(parameters.getRequest())) {
parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT);
}
});
return logoutRequestResolver;
}
@Bean
open fun logoutResponseResolver(registrations: RelyingPartyRegistrationRepository?): Saml2LogoutResponseResolver {
val logoutRequestResolver = OpenSaml5LogoutResponseResolver(registrations)
logoutRequestResolver.setParametersConsumer { LogoutResponseParameters parameters ->
if (checkOtherPrevailingConditions(parameters.getRequest())) {
parameters.getLogoutRequest().getStatus().getStatusCode().setCode(StatusCode.PARTIAL_LOGOUT)
}
}
return logoutRequestResolver
}
然后,您可以在 DSL 中提供您的自定义 Saml2LogoutResponseResolver,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestResolver = this.logoutRequestResolver
}
}
}
自定义 <saml2:LogoutRequest> 认证
要自定义验证,您可以实现自己的 Saml2LogoutRequestValidator。此时,验证是最小的,因此您可能能够首先委托给默认的 Saml2LogoutRequestValidator,如下所示:
-
Java
-
Kotlin
@Component
public class MyOpenSamlLogoutRequestValidator implements Saml2LogoutRequestValidator {
private final Saml2LogoutRequestValidator delegate = new OpenSaml5LogoutRequestValidator();
@Override
public Saml2LogoutRequestValidator logout(Saml2LogoutRequestValidatorParameters parameters) {
// verify signature, issuer, destination, and principal name
Saml2LogoutValidatorResult result = delegate.authenticate(authentication);
LogoutRequest logoutRequest = // ... parse using OpenSAML
// perform custom validation
}
}
@Component
open class MyOpenSamlLogoutRequestValidator: Saml2LogoutRequestValidator {
private val delegate = OpenSaml5LogoutRequestValidator()
@Override
fun logout(parameters: Saml2LogoutRequestValidatorParameters): Saml2LogoutRequestValidator {
// verify signature, issuer, destination, and principal name
val result = delegate.authenticate(authentication)
val logoutRequest: LogoutRequest = // ... parse using OpenSAML
// perform custom validation
}
}
然后,您可以在 DSL 中提供您的自定义 Saml2LogoutRequestValidator,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestValidator(myOpenSamlLogoutRequestValidator)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestValidator = myOpenSamlLogoutRequestValidator
}
}
}
自定义 <saml2:LogoutResponse> 认证
要自定义验证,您可以实现自己的 Saml2LogoutResponseValidator。此时,验证是最小的,因此您可能能够首先委托给默认的 Saml2LogoutResponseValidator,如下所示:
-
Java
-
Kotlin
@Component
public class MyOpenSamlLogoutResponseValidator implements Saml2LogoutResponseValidator {
private final Saml2LogoutResponseValidator delegate = new OpenSaml5LogoutResponseValidator();
@Override
public Saml2LogoutValidatorResult logout(Saml2LogoutResponseValidatorParameters parameters) {
// verify signature, issuer, destination, and status
Saml2LogoutValidatorResult result = delegate.authenticate(parameters);
LogoutResponse logoutResponse = // ... parse using OpenSAML
// perform custom validation
}
}
@Component
open class MyOpenSamlLogoutResponseValidator: Saml2LogoutResponseValidator {
private val delegate = OpenSaml5LogoutResponseValidator()
@Override
fun logout(parameters: Saml2LogoutResponseValidatorParameters): Saml2LogoutResponseValidator {
// verify signature, issuer, destination, and status
val result = delegate.authenticate(authentication)
val logoutResponse: LogoutResponse = // ... parse using OpenSAML
// perform custom validation
}
}
然后,您可以在 DSL 中提供您的自定义 Saml2LogoutResponseValidator,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutResponse((response) -> response
.logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
)
);
http {
saml2Logout {
logoutResponse {
logoutResponseValidator = myOpenSamlLogoutResponseValidator
}
}
}
自定义 <saml2:LogoutRequest> 存储
当您的应用程序发送 <saml2:LogoutRequest> 时,该值会存储在会话中,以便可以验证 RelayState 参数和 <saml2:LogoutResponse> 中的 InResponseTo 属性。
如果您希望将注销请求存储在会话之外的其他位置,您可以在 DSL 中提供您的自定义实现,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestRepository = myCustomLogoutRequestRepository
}
}
}
进一步的注销相关参考
-
CSRF 注意事项部分中的注销