此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.1! |
执行单点注销
在其其他注销机制中,Spring Security 附带了对 RP 和 AP 启动的 SAML 2.0 单点注销的支持。
简而言之,Spring Security 支持两种用例:
-
RP 启动的 - 您的应用程序有一个终端节点,当 POST 到该终端节点时,该终端节点将注销用户并向断言方发送 。 此后,置言方将发回 a 并允许应用程序响应
saml2:LogoutRequest
saml2:LogoutResponse
-
AP 启动的 - 您的应用程序有一个终端节点,该终端节点将从断言方接收 。 您的应用程序将在此时完成注销,然后向断言方发送 。
saml2:LogoutRequest
saml2:LogoutResponse
在 AP 发起的场景中,您的应用程序在注销后执行的任何本地重定向都将变得毫无意义。
一旦应用程序发送 ,它就不再拥有浏览器的控制权。saml2:LogoutResponse |
单点注销的最小配置
要使用 Spring Security 的 SAML 2.0 单点注销功能,您需要满足以下条件:
-
首先,断言方必须支持 SAML 2.0 单点注销
-
其次,应将断言方配置为对应用程序的端点进行签名和 POST 操作
saml2:LogoutRequest
saml2:LogoutResponse
/logout/saml2/slo
-
第三,您的应用程序必须具有 PKCS#8 私钥和用于对 s 和 s 进行签名的 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 | - 用于对 s 和 s 进行签名的签名凭证<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 发起的注销,通过 s 和 s 自动配置自身,以便注销。<saml2:LogoutRequest>
<saml2:LogoutResponse>
它通过确定性启动过程来实现这一点:
-
查询元素的 Identity Server 元数据端点
<SingleLogoutService>
-
扫描元数据并缓存任何公共签名验证密钥
-
准备适当的终端节点
此过程的结果是,Identity Server 必须启动并接收请求,Service Provider 才能成功启动。
如果 Service Provider 查询 Identity Server 时 Identity Server 已关闭(给定适当的超时),则启动将失败。 |
运行时预期
根据上述配置,任何登录用户都可以向您的应用程序发送 以执行 RP 发起的 SLO。
然后,您的应用程序将执行以下操作:POST /logout
-
注销用户并使会话失效
-
生成 a 并将其 POST 到关联的断言方的 SLO 端点
<saml2:LogoutRequest>
-
然后,如果断言方使用 a 进行响应,则应用程序将验证它并重定向到配置的成功端点
<saml2:LogoutResponse>
此外,当断言方向 发送 时,您的应用程序可以参与 AP 发起的注销。
发生这种情况时,您的应用程序将执行以下操作:<saml2:LogoutRequest>
/logout/saml2/slo
-
验证
<saml2:LogoutRequest>
-
注销用户并使会话失效
-
生成一个并将其 POST 回断言方的 SLO 端点
<saml2:LogoutResponse>
最小配置 Sans 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 | - 用于对 s 和 s 进行签名的签名凭证,您也可以将其添加到多个依赖方<saml2:LogoutRequest> <saml2:LogoutResponse> |
4 | - 其次,指示您的应用程序希望使用 SAML SLO 注销最终用户 |
添加后,将注销功能添加到整个服务提供商。
因为它是一项可选功能,所以您需要为每个 .
您可以通过如上所述设置属性来执行此操作。saml2Logout RelyingPartyRegistration RelyingPartyRegistration.Builder#singleLogoutServiceLocation |
Saml 2.0 注销的工作原理
接下来,让我们看看 Spring Security 用于在基于 servlet 的应用程序中支持 SAML 2.0 注销的架构组件,就像我们刚刚看到的一样。
对于 RP 发起的注销:
Spring Security 执行其注销流程,调用其 s 以使会话无效并执行其他清理。
然后,它调用 Saml2RelyingPartyInitiatedLogoutSuccessHandler
。LogoutHandler
注销成功处理程序使用 Saml2LogoutRequestResolver
的实例来创建、签署和序列化 .
它使用与当前 .
然后,它将 重定向到断言方 SLO 端点<saml2:LogoutRequest>
Saml2AuthenticatedPrincipal
<saml2:LogoutRequest>
浏览器将控制权移交给断言方。 如果断言方重定向回去(它可能不会重定向),则应用程序将继续执行步骤 。
Saml2LogoutResponseFilter
使用其 Saml2LogoutResponseValidator
反序列化、验证和处理 。<saml2:LogoutResponse>
如果有效,则它通过重定向到 或已配置的任何内容来完成本地注销流。
如果无效,则以 400 响应。/login?logout
对于 AP 发起的注销:
Saml2LogoutRequestFilter
使用其 Saml2LogoutRequestValidator
反序列化、验证和处理 。<saml2:LogoutRequest>
如果有效,则过滤器将调用配置的 s,使会话失效并执行其他清理。LogoutHandler
它使用 Saml2LogoutResponseResolver
创建、签署和序列化一个 .
它使用从终端节点派生的 RelyingPartyRegistration
或 .
然后,它将 重定向到断言方 SLO 终端节点。<saml2:LogoutResponse>
<saml2:LogoutRequest>
<saml2:LogoutResponse>
浏览器将控制权移交给断言方。
如果无效,则以 400 响应。
配置 Logout 端点
有三种行为可以由不同的终端节点触发:
-
RP 发起的注销,它允许经过身份验证的用户通过向断言方发送
POST
<saml2:LogoutRequest>
-
AP 发起的注销,允许断言方向应用程序发送
<saml2:LogoutRequest>
-
AP 注销响应,允许断言方发送对 RP 发起的响应
<saml2:LogoutResponse>
<saml2:LogoutRequest>
第一个是通过在 principal 类型为 时执行 normal 触发的。POST /logout
Saml2AuthenticatedPrincipal
第二个是通过 POST 到端点触发的,该端点由断言方签名。/logout/saml2/slo
SAMLRequest
第三个是通过 POST 到端点触发的,该端点由断言方签名。/logout/saml2/slo
SAMLResponse
由于用户已登录或原始注销请求已知,因此 已知。
因此, 默认情况下 不是这些 URL 的一部分。registrationId
{registrationId}
此 URL 可在 DSL 中自定义。
例如,如果要将现有的依赖方迁移到 Spring Security,则断言方可能已经指向 。
要减少断言方的配置更改,您可以在 DSL 中配置过滤器,如下所示:GET /SLOService.saml2
-
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"
}
}
现在,如果客户端发送 ,会话将被清除,但不会向断言方发送 。
但是,如果客户端发送 ,则应用程序将照常启动 SAML 2.0 SLO。POST /logout
<saml2:LogoutRequest>
POST /saml2/logout
自定义分辨率<saml2:LogoutRequest>
通常需要在 Spring Security 提供的默认值之外设置其他值。<saml2:LogoutRequest>
默认情况下,Spring Security 将发出 and supply:<saml2:LogoutRequest>
-
属性 - from
Destination
RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation
-
属性 - GUID
ID
-
元素 - 从
<Issuer>
RelyingPartyRegistration#getEntityId
-
元素 - 从
<NameID>
Authentication#getName
要添加其他值,您可以使用 delegation,如下所示:
-
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>
通常需要在 Spring Security 提供的默认值之外设置其他值。<saml2:LogoutResponse>
默认情况下,Spring Security 将发出 and supply:<saml2:LogoutResponse>
-
属性 - from
Destination
RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation
-
属性 - GUID
ID
-
元素 - 从
<Issuer>
RelyingPartyRegistration#getEntityId
-
元素 -
<Status>
SUCCESS
要添加其他值,您可以使用 delegation,如下所示:
-
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>
要自定义验证,您可以实施自己的 .
此时,验证是最少的,因此您可以先委托给 default,如下所示: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>
要自定义验证,您可以实施自己的 .
此时,验证是最少的,因此您可以先委托给 default,如下所示: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 = OpenSamlLogoutResponseValidator()
@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
}
}
}
更多与 Logout 相关的参考
-
在 CSRF 注意事项 一节中注销