执行单点登出
除了其他注销机制外,Spring Security 还提供了对 RP 和 AP 发起的 SAML 2.0 单点注销(Single Logout)的支持。
简而言之,Spring Security 支持两种使用场景:
-
RP 发起的注销 - 您的应用程序拥有一个端点,当向该端点发起 POST 请求时,将注销用户并向断言方发送一个
saml2:LogoutRequest。 随后,断言方将返回一个saml2:LogoutResponse,并允许您的应用程序作出响应。 -
由身份提供方发起(AP-Initiated) - 您的应用程序拥有一个端点,用于接收来自断言方(Asserting Party)的
saml2:LogoutRequest。 此时,您的应用程序将完成其登出操作,并向断言方发送一个saml2:LogoutResponse。
在AP发起(AP-Initiated)场景中,您的应用程序在注销后执行的任何本地重定向都将失效。
一旦您的应用程序发送了saml2:LogoutResponse,它将不再控制浏览器。 |
单注销的最小配置
要使用 Spring Security 的 SAML 2.0 单点注销(Single Logout)功能,您需要具备以下内容:
-
首先,断言方必须支持 SAML 2.0 单点注销(Single Logout)
-
其次,应配置断言方(asserting party)对其发送的
saml2:LogoutRequest和saml2:LogoutResponse进行签名,并通过 POST 方法发送至您应用程序的/logout/saml2/slo端点。 -
第三,您的应用程序必须拥有一个 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 服务提供者(Service Provider)还会自动配置自身,通过 <saml2:LogoutRequest> 和 <saml2:LogoutResponse> 来支持注销操作,无论是由信赖方(RP)发起还是由身份提供者(AP)发起的注销均可。
它通过一个确定性的启动过程来实现这一点:
-
查询 Identity Server 元数据端点以获取
<SingleLogoutService>元素 -
扫描元数据并缓存所有公钥签名验证密钥
-
准备适当的端点
这一过程的一个后果是,身份服务器必须处于运行状态并能够接收请求,服务提供方才能成功启动。
| 如果服务提供方查询身份服务器时(在给定适当的超时设置下)该服务器处于宕机状态,则启动将失败。 |
运行时期望
根据上述配置,任何已登录的用户都可以向您的应用程序发送一个 POST /logout 请求,以执行由依赖方(RP)发起的单点注销(SLO)。
您的应用程序随后将执行以下操作:
-
注销用户并使会话失效
-
生成一个
<saml2:LogoutRequest>并将其通过 POST 请求发送到关联的身份提供方的单点登出(SLO)端点 -
然后,如果断言方返回一个
<saml2:LogoutResponse>,应用程序将验证该响应并重定向到配置的成功端点。
此外,当断言方(Asserting Party)向 <saml2:LogoutRequest> 发送一个 /logout/saml2/slo 时,您的应用程序可以参与由断言方发起的注销(AP-initiated logout)。
发生这种情况时,您的应用程序将执行以下操作:
-
验证
<saml2:LogoutRequest> -
注销用户并使会话失效
-
生成一个
<saml2:LogoutResponse>并通过 POST 方法将其发送回断言方的单点登出(SLO)端点
无需 Boot 的最小化配置
除了使用 Boot 属性外,您也可以通过直接发布 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()
}
}
| 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>.
它使用来自RelyingPartyRegistration与当前关联的Saml2AuthenticatedPrincipal。然后,它将重定向-POST 到<saml2:LogoutRequest>到断言方的 SLO 端点
浏览器将控制权移交给声明方。
如果声明方重定向回来(这可能不会发生),则应用程序进入下一步
.
这Saml2LogoutResponseFilter反序列化、验证并处理<saml2:LogoutResponse>使用其Saml2LogoutResponseValidator.
如果有效,则通过重定向完成本地注销流程/login?logout,或任何已配置的内容。
如果无效,则返回 400 状态码。
对于由身份提供方(AP)发起的注销:
这Saml2LogoutRequestFilter反序列化、验证并处理<saml2:LogoutRequest>使用其Saml2LogoutRequestValidator.
如果有效,则过滤器将调用配置的LogoutHandlers,使会话失效并执行其他清理操作。
它使用Saml2LogoutResponseResolver创建、签名和序列化<saml2:LogoutResponse>.
它使用来自RelyingPartyRegistration源自端点或来自内容的<saml2:LogoutRequest>。然后,它将重定向-POST 到<saml2:LogoutResponse>到断言方的 SLO 端点。
浏览器将控制权交给断言方。
如果无效,则它返回 400 错误.
配置注销端点
有三种行为可由不同的端点触发:
-
RP 发起的注销(RP-initiated logout),允许已认证用户通过
POST请求发送一个<saml2:LogoutRequest>给断言方,从而触发注销流程。 -
由身份提供方(AP)发起的注销,允许断言方(asserting party)向应用程序发送一个
<saml2:LogoutRequest> -
AP 注销响应,允许断言方(Asserting Party)在收到由依赖方(RP)发起的
<saml2:LogoutResponse>后,发送一个<saml2:LogoutRequest>作为响应。
第一种情况是在主体类型为 POST /logout 时,执行正常的 Saml2AuthenticatedPrincipal 请求所触发的。
第二种方式是向 /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 中的 InResponseTo 参数和 <saml2:LogoutResponse> 属性。
如果你想将注销请求存储在会话以外的其他位置,可以在 DSL 中提供你自己的实现,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestRepository = myCustomLogoutRequestRepository
}
}
}
其他与注销相关的参考
-
注销 位于 CSRF 注意事项 章节中