跳到主要内容
版本:7.0.2

执行单点登出

DeepSeek V3 中英对照 SAML2 Logout Performing Single Logout

除了其他登出机制外,Spring Security 还支持 RP 和 AP 发起的 SAML 2.0 单点登出。

简而言之,Spring Security 支持两种使用场景:

  • RP 发起 - 您的应用程序有一个端点,当向其发送 POST 请求时,将注销用户并向断言方发送一个 saml2:LogoutRequest。此后,断言方将发回一个 saml2:LogoutResponse 并允许您的应用程序进行响应。

  • AP 发起 - 您的应用程序有一个端点,将接收来自断言方的 saml2:LogoutRequest。您的应用程序将在此时完成其注销,然后向断言方发送一个 saml2:LogoutResponse

备注

AP-Initiated 场景中,应用程序在注销后可能进行的任何本地重定向都将失效。一旦您的应用程序发送了 saml2:LogoutResponse,它将不再控制浏览器。

单点登出的最小配置

要使用 Spring Security 的 SAML 2.0 单点登出功能,你需要具备以下条件:

  • 首先,断言方必须支持 SAML 2.0 单点登出

  • 其次,需要将断言方配置为对发送至您应用程序 /logout/saml2/slo 端点的 saml2:LogoutRequestsaml2:LogoutResponse 进行签名并使用 POST 方法发送

  • 第三,您的应用程序必须拥有用于对 saml2:LogoutRequestsaml2: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)发起的注销均可实现。

它通过一个确定性的启动过程来实现这一点:

  1. 查询身份服务器元数据端点以获取 <SingleLogoutService> 元素

  2. 扫描元数据并缓存所有公共签名验证密钥

  3. 准备相应的端点

这一流程的结果是,身份服务器必须处于运行状态并能够接收请求,服务提供者才能成功启动。

备注

如果身份服务器在服务提供商查询时宕机(在适当的超时时间内),那么启动将会失败。

运行时预期

根据上述配置,任何已登录用户都可以向您的应用程序发送 POST /logout 请求来执行服务提供方发起的单点登出。您的应用程序随后将执行以下操作:

  1. 注销用户并使会话失效

  2. 生成 <saml2:LogoutRequest> 并将其 POST 到关联断言方的 SLO 端点

  3. 然后,如果断言方响应 <saml2:LogoutResponse>,应用程序将验证该响应并重定向到配置的成功端点

此外,当断言方(asserting party)向 /logout/saml2/slo 发送 <saml2:LogoutRequest> 时,您的应用程序可以参与由身份提供者(AP)发起的注销流程。此时,您的应用程序将执行以下操作:

  1. 验证 <saml2:LogoutRequest>

  2. 注销用户并使会话失效

  3. 生成 <saml2:LogoutResponse> 并将其 POST 回断言方的 SLO 端点

最小配置(无引导)

除了使用启动属性,你也可以通过直接发布Bean来实现相同的结果,如下所示:

@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();
}
}
  • IDP 的元数据 URI,它将向您的应用程序表明其支持 SLO

  • 您应用程序中的 SLO 端点

  • 用于签署 <saml2:LogoutRequest><saml2:LogoutResponse> 的签名凭据,您也可以将其添加到多个依赖方

  • 其次,表明您的应用程序希望使用 SAML SLO 来注销最终用户

备注

添加 saml2Logout 为你的服务提供者整体增加了注销能力。由于这是一个可选功能,你需要为每个单独的 RelyingPartyRegistration 启用它。你可以通过如上所示设置 RelyingPartyRegistration.Builder#singleLogoutServiceLocation 属性来实现。

Saml 2.0 注销工作原理

接下来,我们来看看 Spring Security 在基于 Servlet 的应用程序(例如我们刚刚看到的那个)中,用于支持 SAML 2.0 注销 的架构组件。

对于RP发起的注销:

1 Spring Security 执行其登出流程,调用其 LogoutHandler 以作废会话并执行其他清理操作。随后,它会调用 Saml2RelyingPartyInitiatedLogoutSuccessHandler

2 登出成功处理器使用 Saml2LogoutRequestResolver 的实例来创建、签名并序列化一个 <saml2:LogoutRequest>。它使用与当前 Saml2AuthenticatedPrincipal 关联的 RelyingPartyRegistration 中的密钥和配置。然后,它将 <saml2:LogoutRequest> 通过重定向 POST 到断言方 SLO 端点。

浏览器将控制权移交给断言方。如果断言方重定向回来(可能不会),则应用程序继续执行步骤 3

3 Saml2LogoutResponseFilter 使用其 Saml2LogoutResponseValidator<saml2:LogoutResponse> 进行反序列化、验证和处理。

4 若验证有效,则通过重定向至 /login?logout(或已配置的其他路径)来完成本地登出流程。若验证无效,则返回 400 状态码作为响应。

对于AP发起的注销:

1 Saml2LogoutRequestFilter 使用其 Saml2LogoutRequestValidator<saml2:LogoutRequest> 进行反序列化、验证和处理。

2 若验证有效,过滤器将调用配置的 LogoutHandler,执行会话失效及其他清理操作。

3 它使用 Saml2LogoutResponseResolver 来创建、签名并序列化一个 <saml2:LogoutResponse>。它使用从端点或 <saml2:LogoutRequest> 内容派生的 RelyingPartyRegistration 中的密钥和配置。然后,它将 <saml2:LogoutResponse> 通过重定向 POST 到断言方 SLO 端点。

浏览器将控制权移交给断言方。

4 如果无效,则返回 400 响应

配置登出端点

有三种行为可以通过不同的端点触发:

  • RP 发起的登出,允许已认证用户通过向断言方发送 <saml2:LogoutRequest>POST 并触发登出流程

  • 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中这样配置过滤器:

http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request.logoutUrl("/SLOService.saml2"))
.logoutResponse((response) -> response.logoutUrl("/SLOService.saml2"))
);

您还应在 RelyingPartyRegistration 中配置这些端点。

此外,你也可以像这样自定义本地触发登出的端点:

http
.saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));

分离本地登出与 SAML 2.0 登出

在某些情况下,您可能希望为本地登出暴露一个端点,而为RP发起的单点登出(SLO)暴露另一个端点。与其他登出机制类似,只要每个端点不同,您可以注册多个登出端点。

因此,举例来说,你可以像这样连接DSL:

http
.logout((logout) -> logout.logoutUrl("/logout"))
.saml2Logout((saml2) -> saml2.logoutUrl("/saml2/logout"));

现在,如果客户端发送一个 POST /logout 请求,会话将被清除,但不会向断言方发送 <saml2:LogoutRequest>。然而,如果客户端发送一个 POST /saml2/logout 请求,那么应用程序将正常启动 SAML 2.0 单点注销流程。

自定义 <saml2:LogoutRequest> 解析

通常需要在 <saml2:LogoutRequest> 中设置 Spring Security 默认值之外的其他值。

默认情况下,Spring Security 将发出 <saml2:LogoutRequest> 并提供:

  • DestinationValidator 属性 - 来自 RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation

  • ID 属性 - 一个 GUID

  • <Issuer> 元素 - 来自 RelyingPartyRegistration#getEntityId

  • <NameID> 元素 - 来自 Authentication#getName

要添加其他值,你可以使用委托,如下所示:

@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;
}

然后,你可以在DSL中提供自定义的Saml2LogoutRequestResolver,如下所示:

http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);

自定义 <saml2:LogoutResponse> 解析

通常需要在 <saml2:LogoutResponse> 中设置 Spring Security 默认值之外的其他值。

默认情况下,Spring Security 将发出 <saml2:LogoutResponse> 并提供:

  • DestinationValidator 属性 - 来自 RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation

  • ID 属性 - 一个 GUID

  • <Issuer> 元素 - 来自 RelyingPartyRegistration#getEntityId

  • <Status> 元素 - SUCCESS

要添加其他值,你可以使用委托,如下所示:

@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;
}

然后,你可以在DSL中提供自定义的Saml2LogoutResponseResolver,如下所示:

http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestResolver(this.logoutRequestResolver)
)
);

自定义 <saml2:LogoutRequest> 认证

要自定义验证逻辑,您可以实现自己的 Saml2LogoutRequestValidator。目前验证逻辑较为简单,因此您可以先委托给默认的 Saml2LogoutRequestValidator,如下所示:

@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
}
}

然后,你可以在DSL中提供自定义的Saml2LogoutRequestValidator,如下所示:

http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestValidator(myOpenSamlLogoutRequestValidator)
)
);

自定义 <saml2:LogoutResponse> 认证

要自定义验证逻辑,您可以实现自己的 Saml2LogoutResponseValidator。目前验证逻辑较为基础,因此您可以先委托给默认的 Saml2LogoutResponseValidator,例如:

@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
}
}

然后,你可以在DSL中提供自定义的Saml2LogoutResponseValidator,如下所示:

http
.saml2Logout((saml2) -> saml2
.logoutResponse((response) -> response
.logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
)
);

自定义 <saml2:LogoutRequest> 存储

当您的应用程序发送 <saml2:LogoutRequest> 时,其值会存储在会话中,以便验证 <saml2:LogoutResponse> 中的 RelayState 参数和 InResponseTo 属性。

如果你想将登出请求存储在会话之外的地方,可以在DSL中提供自定义实现,如下所示:

http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);

更多与登出相关的参考资料