在其他注销机制中,Spring Security 支持 RP 和 AP 发起的 SAML 2.0 单点注销。Spring中文文档

简而言之,Spring Security 支持两种用例:Spring中文文档

  • RP-Initiated - 应用程序具有一个终结点,当 POST 到该终结点时,该终结点将注销用户并向断言方发送一个终结点。 此后,主张方将发回 a 并允许您的申请做出回应saml2:LogoutRequestsaml2:LogoutResponseSpring中文文档

  • AP-Initiated - 应用程序具有一个终结点,该终结点将从断言方接收 AP-Initiated - 应用程序具有将从断言方接收的终结点。 此时,应用程序将完成注销,然后向断言方发送注销。saml2:LogoutRequestsaml2:LogoutResponseSpring中文文档

AP 启动的方案中,应用程序在注销后执行的任何本地重定向都变得毫无意义。 一旦应用程序发送 ,它就不再控制浏览器。saml2:LogoutResponse
AP 启动的方案中,应用程序在注销后执行的任何本地重定向都变得毫无意义。 一旦应用程序发送 ,它就不再控制浏览器。saml2:LogoutResponse

单点注销的最低配置

要使用 Spring Security 的 SAML 2.0 单点注销功能,您需要满足以下条件:Spring中文文档

  • 首先,断言方必须支持 SAML 2.0 单点注销Spring中文文档

  • 其次,应将断言方配置为对应用程序的终结点进行签名和开机处理saml2:LogoutRequestsaml2:LogoutResponse/logout/saml2/sloSpring中文文档

  • 第三,应用程序必须具有 PKCS#8 私钥和 X.509 证书,用于对 s 和 s 进行签名saml2:LogoutRequestsaml2:LogoutResponseSpring中文文档

你可以通过以下方式在Spring Boot中实现这一点:Spring中文文档

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中文文档

Spring Security 的注销支持提供了许多配置点。 请考虑以下用例:Spring中文文档

创业期望

使用这些属性时,除了登录之外,SAML 2.0 服务提供商还将自动配置自身,以便使用 RP 或 AP 启动的注销方式通过 s 和 s 进行注销。<saml2:LogoutRequest><saml2:LogoutResponse>Spring中文文档

它通过确定性的启动过程实现了这一点:Spring中文文档

  1. 查询元素的 Identity Server 元数据端点<SingleLogoutService>Spring中文文档

  2. 扫描元数据并缓存任何公共签名验证密钥Spring中文文档

  3. 准备适当的终结点Spring中文文档

此过程的结果是,身份服务器必须启动并接收请求,服务提供商才能成功启动。Spring中文文档

如果身份服务器在服务提供商查询时已关闭(给定适当的超时),则启动将失败。

运行时期望

根据上述配置,任何登录用户都可以向应用程序发送 A 以执行 RP 启动的 SLO。 然后,应用程序将执行以下操作:POST /logoutSpring中文文档

  1. 注销用户并使会话失效Spring中文文档

  2. 生成一个并将其 POST 到关联的断言方的 SLO 终结点<saml2:LogoutRequest>Spring中文文档

  3. 然后,如果断言方使用 响应,则应用程序将验证它并重定向到配置的成功终结点<saml2:LogoutResponse>Spring中文文档

此外,当断言方将 发送到 时,应用程序可以参与 AP 发起的注销。 发生这种情况时,应用程序将执行以下操作:<saml2:LogoutRequest>/logout/saml2/sloSpring中文文档

  1. 验证<saml2:LogoutRequest>Spring中文文档

  2. 注销用户并使会话失效Spring中文文档

  3. 生成一个并将其 POST 回置方的 SLO 端点<saml2:LogoutResponse>Spring中文文档

1 - IDP 的元数据 URI,它将向你的应用程序指示其对 SLO 的支持
2 - 应用程序中的 SLO 端点
3 - 用于对 s 和 s 进行签名的签名凭据<saml2:LogoutRequest><saml2:LogoutResponse>
如果身份服务器在服务提供商查询时已关闭(给定适当的超时),则启动将失败。

最小配置无启动

除了 Boot 属性之外,您还可以通过直接发布 Bean 来实现相同的结果,如下所示:Spring中文文档

@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 注销最终用户
添加将注销功能添加到整个服务提供商。 因为它是一项可选功能,所以您需要为每个人启用它。 为此,您可以按上述方式设置属性。saml2LogoutRelyingPartyRegistrationRelyingPartyRegistration.Builder#singleLogoutServiceLocation
1 - IDP 的元数据 URI,它将向你的应用程序指示其对 SLO 的支持
2 - 应用程序中的 SLO 端点
3 - 用于对 s 和 s 进行签名的签名凭据,还可以将其添加到多个信赖方<saml2:LogoutRequest><saml2:LogoutResponse>
4 - 其次,指示您的应用程序希望使用 SAML SLO 注销最终用户
添加将注销功能添加到整个服务提供商。 因为它是一项可选功能,所以您需要为每个人启用它。 为此,您可以按上述方式设置属性。saml2LogoutRelyingPartyRegistrationRelyingPartyRegistration.Builder#singleLogoutServiceLocation

Saml 2.0 注销的工作原理

接下来,让我们看看Spring Security在基于servlet的应用程序中用于支持SAML 2.0 Logout的架构组件,就像我们刚才看到的一样。Spring中文文档

对于 RP 启动的注销:Spring中文文档

数字 1Spring Security 执行其注销流程,调用其 s 以使会话无效并执行其他清理。 然后,它调用 Saml2RelyingPartyInitiatedLogoutSuccessHandlerLogoutHandlerSpring中文文档

数字 2注销成功处理程序使用 Saml2LogoutRequestResolver 的实例创建、签名和序列化 . 它使用与当前 . 关联的 RelyingPartyRegistration 中的键和配置。 然后,它将 POST 重定向到断言方 SLO 终结点<saml2:LogoutRequest>Saml2AuthenticatedPrincipal<saml2:LogoutRequest>Spring中文文档

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

数字 3Saml2LogoutResponseFilter 使用其 Saml2LogoutResponseValidator 反序列化、验证和处理 。<saml2:LogoutResponse>Spring中文文档

数字 4如果有效,则通过重定向到 或已配置的任何内容来完成本地注销流。 如果无效,则以 400 响应。/login?logoutSpring中文文档

对于 AP 发起的注销:Spring中文文档

数字 1Saml2LogoutRequestFilter 使用其 Saml2LogoutRequestValidator 反序列化、验证和处理 。<saml2:LogoutRequest>Spring中文文档

数字 2如果有效,则筛选器将调用配置的 s,使会话无效并执行其他清理。LogoutHandlerSpring中文文档

数字 3它使用 Saml2LogoutResponseResolver 创建、签名和序列化 . 它使用从终结点派生的 RelyingPartyRegistration 或 . 然后,它将 POST 重定向到断言方 SLO 终结点。<saml2:LogoutResponse><saml2:LogoutRequest><saml2:LogoutResponse>Spring中文文档

浏览器将控制权移交给断言方。Spring中文文档

数字 4如果无效,则以 400 响应Spring中文文档

配置注销终结点

有三种行为可以由不同的终结点触发:Spring中文文档

  • RP 发起的注销,允许经过身份验证的用户通过向断言方发送POST<saml2:LogoutRequest>Spring中文文档

  • AP 发起的注销,允许断言方向应用程序发送<saml2:LogoutRequest>Spring中文文档

  • AP 注销响应,允许断言方发送响应 RP 发起的<saml2:LogoutResponse><saml2:LogoutRequest>Spring中文文档

第一种是通过在主体类型为 时执行正常触发的。POST /logoutSaml2AuthenticatedPrincipalSpring中文文档

第二个是通过 POST 到端点触发的,并由断言方签名。/logout/saml2/sloSAMLRequestSpring中文文档

第三个是通过 POST 到终端节点触发的,并由断言方签名。/logout/saml2/sloSAMLResponseSpring中文文档

由于用户已登录或原始注销请求已知,因此已知道。 因此,默认情况下不是这些 URL 的一部分。registrationId{registrationId}Spring中文文档

此 URL 可在 DSL 中自定义。Spring中文文档

例如,如果要将现有信赖方迁移到 Spring Security,则断言方可能已经指向 . 若要减少断言方的配置更改,可以在 DSL 中配置筛选器,如下所示:GET /SLOService.saml2Spring中文文档

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

您还应该在 .RelyingPartyRegistrationSpring中文文档

此外,还可以自定义用于在本地触发注销的终结点,如下所示:Spring中文文档

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

将本地注销与 SAML 2.0 注销分开

在某些情况下,您可能希望为本地注销公开一个注销终结点,并为 RP 启动的 SLO 公开另一个注销终结点。 与其他注销机制一样,您可以注册多个注销机制,只要它们每个都有不同的终结点即可。Spring中文文档

因此,例如,您可以像这样连接 DSL:Spring中文文档

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/logoutSpring中文文档

自定义分辨率<saml2:LogoutRequest>

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

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

若要添加其他值,可以使用委派,如下所示:Spring中文文档

@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 中提供自定义,如下所示:Saml2LogoutRequestResolverSpring中文文档

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

自定义分辨率<saml2:LogoutResponse>

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

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

若要添加其他值,可以使用委派,如下所示:Spring中文文档

@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 中提供自定义,如下所示:Saml2LogoutResponseResolverSpring中文文档

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

自定义身份验证<saml2:LogoutRequest>

要自定义验证,您可以实现自己的 . 此时,验证是最小的,因此您可以先委托给默认值,如下所示:Saml2LogoutRequestValidatorSaml2LogoutRequestValidatorSpring中文文档

@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 中提供自定义,如下所示:Saml2LogoutRequestValidatorSpring中文文档

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

自定义身份验证<saml2:LogoutResponse>

要自定义验证,您可以实现自己的 . 此时,验证是最小的,因此您可以先委托给默认值,如下所示:Saml2LogoutResponseValidatorSaml2LogoutResponseValidatorSpring中文文档

@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 中提供自定义,如下所示:Saml2LogoutResponseValidatorSpring中文文档

http
    .saml2Logout((saml2) -> saml2
        .logoutResponse((response) -> response
            .logoutResponseAuthenticator(myOpenSamlLogoutResponseAuthenticator)
        )
    );
http {
    saml2Logout {
        logoutResponse {
            logoutResponseValidator = myOpenSamlLogoutResponseValidator
        }
    }
}

自定义存储<saml2:LogoutRequest>

当应用程序发送 时,该值将存储在会话中,以便可以验证 中的参数和属性。<saml2:LogoutRequest>RelayStateInResponseTo<saml2:LogoutResponse>Spring中文文档

如果要将注销请求存储在会话以外的其他位置,可以在 DSL 中提供自定义实现,如下所示:Spring中文文档

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