执行单点注销
Spring Security 除了提供其他注销机制之外,还支持 RP 和 AP 发起的 SAML 2.0 单点注销。
简而言之,Spring Security 支持两种使用场景:
-
RP-Initiated - 您的应用程序有一个端点,当向其发送POST请求时,将注销用户并向身份提供方发送
saml2:LogoutRequest
。此后,身份提供方将返回一个saml2:LogoutResponse
,并允许您的应用程序作出响应。 -
AP-Initiated - 您的应用程序有一个端点,该端点将接收来自身份提供方的
saml2:LogoutRequest
。您的应用程序将在那时完成注销,然后向身份提供方发送saml2:LogoutResponse
。
在AP-Initiated场景中,你的应用程序在登出后进行的任何本地重定向都将变得无效。一旦你的应用程序发送了saml2:LogoutResponse
,它就不再控制浏览器。
单点登出的最小配置
要使用 Spring Security 的 SAML 2.0 单点注销功能,您需要以下内容:
-
首先,声明方必须支持 SAML 2.0 单点注销
-
其次,声明方应配置为对
saml2:LogoutRequest
和saml2:LogoutResponse
进行签名并 POST 到您的应用程序的/logout/saml2/slo
端点 -
第三,您的应用程序必须具有用于签名
saml2:LogoutRequest
和saml2:LogoutResponse
的 PKCS#8 私钥和 X.509 证书
您可以通过以下方式在 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>
- IDP 的元数据 URI,它将向您的应用程序指示其对 SLO 的支持
- 您的应用程序中的 SLO 端点
- 用于签名
<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 服务提供者将自动配置自身,通过 <saml2:LogoutRequest>
和 <saml2:LogoutResponse>
来实现注销,可以使用 RP 或 AP 发起的注销。
它通过一个确定性的启动过程来实现这一点:
-
查询 Identity Server 的 Metadata 端点以获取
<SingleLogoutService>
元素 -
扫描元数据并缓存任何公共签名验证密钥
-
准备适当的端点
这个过程的一个结果是,身份服务器必须启动并接收请求,以便服务提供商能够成功启动。
如果 Service Provider 在查询时身份服务器宕机(假设设置了适当的超时时间),则启动将失败。
运行时预期
根据上述配置,任何已登录的用户都可以向您的应用程序发送 POST /logout
以执行由RP发起的SLO。然后,您的应用程序将执行以下操作:
-
注销用户并使会话失效
-
生成一个
<saml2:LogoutRequest>
并将其 POST 到关联的断言方的 SLO 端点 -
然后,如果断言方响应了一个
<saml2:LogoutResponse>
,应用程序将验证它,并重定向到配置的成功端点
此外,当断言方将 <saml2:LogoutRequest>
发送到 /logout/saml2/slo
时,您的应用程序可以参与由 AP 发起的注销。当这种情况发生时,您的应用程序将执行以下操作:
-
验证
<saml2:LogoutRequest>
-
注销用户并使会话失效
-
生成
<saml2:LogoutResponse>
并将其 POST 回断言方的 SLO 端点
最小配置(不包括引导)
你可以通过直接发布 bean 来达到同样的效果,如下所示:
- 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()
}
}
- IDP 的元数据 URI,它将向您的应用程序表明其对 SLO 的支持
- 您的应用程序中的 SLO 端点
- 用于签署
<saml2:LogoutRequest>
和<saml2:LogoutResponse>
的签名凭据,您也可以将其添加到多个依赖方- 其次,表明您的应用程序希望使用 SAML SLO 来注销终端用户
添加 saml2Logout
为您的服务提供商整体增加了注销功能。由于这是一个可选功能,您需要为每个单独的 RelyingPartyRegistration
启用它。您可以通过设置 RelyingPartyRegistration.Builder#singleLogoutServiceLocation
属性来实现这一点,如上所示。
Saml 2.0 注销的工作原理
接下来,让我们看看Spring Security使用的架构组件,以支持基于servlet的应用程序(如我们刚刚看到的)中的SAML 2.0 Logout。
对于RP发起的注销:
1 Spring Security 执行其注销流程,调用其 LogoutHandler
来使会话无效并执行其他清理工作。然后它调用Saml2RelyingPartyInitiatedLogoutSuccessHandler。
2 注销成功处理程序使用 Saml2LogoutRequestResolver 的实例来创建、签名和序列化一个 <saml2:LogoutRequest>
。它使用与当前 Saml2AuthenticatedPrincipal
关联的 RelyingPartyRegistration 中的密钥和配置。然后,它通过重定向-POST 将 <saml2:LogoutRequest>
发送到断言方 SLO 端点。
浏览器将控制权交给声明方。如果声明方重定向回来(它可能不会),则应用程序继续执行步骤 3 。
3 Saml2LogoutResponseFilter 反序列化、验证并处理 <saml2:LogoutResponse>
,使用其 Saml2LogoutResponseValidator。
4 如果有效,则通过重定向到 /login?logout
或其他已配置的地址来完成本地登出流程。如果无效,则返回 400 响应。
对于由AP发起的注销:
1 Saml2LogoutRequestFilter 反序列化、验证并处理 <saml2:LogoutRequest>
,使用其 Saml2LogoutRequestValidator。
2 如果有效,则过滤器会调用配置的 LogoutHandler
,使会话失效并执行其他清理工作。
3 它使用 Saml2LogoutResponseResolver 来创建、签名和序列化一个 <saml2:LogoutResponse>
。它使用从端点派生的 RelyingPartyRegistration 或 <saml2:LogoutRequest>
的内容中的密钥和配置。然后,它通过重定向-POST 将 <saml2:LogoutResponse>
发送到断言方 SLO 端点。
浏览器将控制权交给声明方。
4 如果无效,则它响应 400。
配置注销端点
有三种行为可以由不同的端点触发:
-
RP发起的注销,允许已认证的用户通过发送
<saml2:LogoutRequest>
给声明方来POST
并触发注销过程 -
AP发起的注销,允许声明方向应用程序发送
<saml2:LogoutRequest>
-
AP注销响应,允许声明方在响应RP发起的
<saml2:LogoutRequest>
时发送<saml2:LogoutResponse>
第一种是在主体(principal)类型为 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>
并提供:
-
Destination
属性 - 来自RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation
-
ID
属性 - 一个 GUID -
<Issuer>
元素 - 来自RelyingPartyRegistration#getEntityId
-
<NameID>
元素 - 来自Authentication#getName
要添加其他值,你可以使用委托,如下所示:
- Java
- Kotlin
@Bean
Saml2LogoutRequestResolver logoutRequestResolver(RelyingPartyRegistrationRepository registrations) {
OpenSaml4LogoutRequestResolver logoutRequestResolver =
new OpenSaml4LogoutRequestResolver(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 = OpenSaml4LogoutRequestResolver(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>
并提供:
-
Destination
属性 - 来自RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation
-
ID
属性 - 一个 GUID -
<Issuer>
元素 - 来自RelyingPartyRegistration#getEntityId
-
<Status>
元素 -SUCCESS
要添加其他值,你可以使用委托,如下所示:
- Java
- Kotlin
@Bean
public Saml2LogoutResponseResolver logoutResponseResolver(RelyingPartyRegistrationRepository registrations) {
OpenSaml4LogoutResponseResolver logoutRequestResolver =
new OpenSaml4LogoutResponseResolver(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 = OpenSaml4LogoutResponseResolver(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 OpenSamlLogoutRequestValidator();
@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 = OpenSamlLogoutRequestValidator()
@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 OpenSamlLogoutResponseValidator();
@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 = OpenSaml4LogoutResponseValidator()
@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>
时,该值将存储在会话中,以便验证 <saml2:LogoutResponse>
中的 RelayState
参数和 InResponseTo
属性。
如果你希望将登出请求存储在会话之外的其他地方,你可以在DSL中提供自定义实现,如下所示:
- Java
- Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestRepository = myCustomLogoutRequestRepository
}
}
}
进一步的登出相关参考
-
登出 在章节 CSRF 注意事项中