执行单点登出

除了其他注销机制外,Spring Security 还提供了对 RP 和 AP 发起的 SAML 2.0 单点注销(Single Logout)的支持。spring-doc.cadn.net.cn

简而言之,Spring Security 支持两种使用场景:spring-doc.cadn.net.cn

  • RP 发起的注销 - 您的应用程序拥有一个端点,当向该端点发起 POST 请求时,将注销用户并向断言方发送一个 saml2:LogoutRequest。 随后,断言方将返回一个 saml2:LogoutResponse,并允许您的应用程序作出响应。spring-doc.cadn.net.cn

  • 由身份提供方发起(AP-Initiated) - 您的应用程序拥有一个端点,用于接收来自断言方(Asserting Party)的 saml2:LogoutRequest。 此时,您的应用程序将完成其登出操作,并向断言方发送一个 saml2:LogoutResponsespring-doc.cadn.net.cn

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

单注销的最小配置

要使用 Spring Security 的 SAML 2.0 单点注销(Single Logout)功能,您需要具备以下内容:spring-doc.cadn.net.cn

  • 首先,断言方必须支持 SAML 2.0 单点注销(Single Logout)spring-doc.cadn.net.cn

  • 其次,应配置断言方(asserting party)对其发送的 saml2:LogoutRequestsaml2:LogoutResponse 进行签名,并通过 POST 方法发送至您应用程序的 /logout/saml2/slo 端点。spring-doc.cadn.net.cn

  • 第三,您的应用程序必须拥有一个 PKCS#8 私钥和 X.509 证书,用于对 saml2:LogoutRequestsaml2:LogoutResponse 进行签名。spring-doc.cadn.net.cn

你可以在 Spring Boot 中通过以下方式实现这一点:spring-doc.cadn.net.cn

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-doc.cadn.net.cn

Spring Security 的注销支持提供了多个配置点。 请考虑以下使用场景:spring-doc.cadn.net.cn

启动预期

当使用这些属性时,除了登录功能外,SAML 2.0 服务提供者(Service Provider)还会自动配置自身,通过 <saml2:LogoutRequest><saml2:LogoutResponse> 来支持注销操作,无论是由信赖方(RP)发起还是由身份提供者(AP)发起的注销均可。spring-doc.cadn.net.cn

它通过一个确定性的启动过程来实现这一点:spring-doc.cadn.net.cn

  1. 查询 Identity Server 元数据端点以获取 <SingleLogoutService> 元素spring-doc.cadn.net.cn

  2. 扫描元数据并缓存所有公钥签名验证密钥spring-doc.cadn.net.cn

  3. 准备适当的端点spring-doc.cadn.net.cn

这一过程的一个后果是,身份服务器必须处于运行状态并能够接收请求,服务提供方才能成功启动。spring-doc.cadn.net.cn

如果服务提供方查询身份服务器时(在给定适当的超时设置下)该服务器处于宕机状态,则启动将失败。

运行时期望

根据上述配置,任何已登录的用户都可以向您的应用程序发送一个 POST /logout 请求,以执行由依赖方(RP)发起的单点注销(SLO)。 您的应用程序随后将执行以下操作:spring-doc.cadn.net.cn

  1. 注销用户并使会话失效spring-doc.cadn.net.cn

  2. 生成一个 <saml2:LogoutRequest> 并将其通过 POST 请求发送到关联的身份提供方的单点登出(SLO)端点spring-doc.cadn.net.cn

  3. 然后,如果断言方返回一个 <saml2:LogoutResponse>,应用程序将验证该响应并重定向到配置的成功端点。spring-doc.cadn.net.cn

此外,当断言方(Asserting Party)向 <saml2:LogoutRequest> 发送一个 /logout/saml2/slo 时,您的应用程序可以参与由断言方发起的注销(AP-initiated logout)。 发生这种情况时,您的应用程序将执行以下操作:spring-doc.cadn.net.cn

  1. 验证 <saml2:LogoutRequest>spring-doc.cadn.net.cn

  2. 注销用户并使会话失效spring-doc.cadn.net.cn

  3. 生成一个 <saml2:LogoutResponse> 并通过 POST 方法将其发送回断言方的单点登出(SLO)端点spring-doc.cadn.net.cn

无需 Boot 的最小化配置

除了使用 Boot 属性外,您也可以通过直接发布 Bean 来实现相同的效果,如下所示:spring-doc.cadn.net.cn

@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 注销的架构组件。spring-doc.cadn.net.cn

对于由RP发起的注销:spring-doc.cadn.net.cn

number 1Spring Security 执行其注销流程,调用其LogoutHandler用于使会话失效并执行其他清理操作。 随后它调用Saml2RelyingPartyInitiatedLogoutSuccessHandler.spring-doc.cadn.net.cn

number 2登出成功处理器使用了一个实例Saml2LogoutRequestResolver创建、签名和序列化<saml2:LogoutRequest>. 它使用来自RelyingPartyRegistration与当前关联的Saml2AuthenticatedPrincipal。然后,它将重定向-POST 到<saml2:LogoutRequest>到断言方的 SLO 端点spring-doc.cadn.net.cn

浏览器将控制权移交给声明方。 如果声明方重定向回来(这可能不会发生),则应用程序进入下一步number 3.spring-doc.cadn.net.cn

number 3Saml2LogoutResponseFilter反序列化、验证并处理<saml2:LogoutResponse>使用其Saml2LogoutResponseValidator.spring-doc.cadn.net.cn

number 4如果有效,则通过重定向完成本地注销流程/login?logout,或任何已配置的内容。 如果无效,则返回 400 状态码。spring-doc.cadn.net.cn

对于由身份提供方(AP)发起的注销:spring-doc.cadn.net.cn

number 1Saml2LogoutRequestFilter反序列化、验证并处理<saml2:LogoutRequest>使用其Saml2LogoutRequestValidator.spring-doc.cadn.net.cn

number 2如果有效,则过滤器将调用配置的LogoutHandlers,使会话失效并执行其他清理操作。spring-doc.cadn.net.cn

number 3它使用Saml2LogoutResponseResolver创建、签名和序列化<saml2:LogoutResponse>. 它使用来自RelyingPartyRegistration源自端点或来自内容的<saml2:LogoutRequest>。然后,它将重定向-POST 到<saml2:LogoutResponse>到断言方的 SLO 端点。spring-doc.cadn.net.cn

浏览器将控制权交给断言方。spring-doc.cadn.net.cn

配置注销端点

有三种行为可由不同的端点触发:spring-doc.cadn.net.cn

  • RP 发起的注销(RP-initiated logout),允许已认证用户通过 POST 请求发送一个 <saml2:LogoutRequest> 给断言方,从而触发注销流程。spring-doc.cadn.net.cn

  • 由身份提供方(AP)发起的注销,允许断言方(asserting party)向应用程序发送一个 <saml2:LogoutRequest>spring-doc.cadn.net.cn

  • AP 注销响应,允许断言方(Asserting Party)在收到由依赖方(RP)发起的 <saml2:LogoutResponse> 后,发送一个 <saml2:LogoutRequest> 作为响应。spring-doc.cadn.net.cn

第一种情况是在主体类型为 POST /logout 时,执行正常的 Saml2AuthenticatedPrincipal 请求所触发的。spring-doc.cadn.net.cn

第二种方式是向 /logout/saml2/slo 端点发送一个 POST 请求,其中包含由断言方签名的 SAMLRequestspring-doc.cadn.net.cn

第三种方式是向 /logout/saml2/slo 端点发送 POST 请求,并附带由断言方签名的 SAMLResponsespring-doc.cadn.net.cn

由于用户已经登录,或者原始的注销请求已知,因此registrationId已经是已知的。 出于这个原因,默认情况下,{registrationId}不会包含在这些URL中。spring-doc.cadn.net.cn

此 URL 在 DSL 中是可自定义的。spring-doc.cadn.net.cn

例如,如果您正在将现有的依赖方迁移到 Spring Security,您的断言方可能已经指向 GET /SLOService.saml2。 为了减少断言方配置的变更,您可以在 DSL 中按如下方式配置该过滤器:spring-doc.cadn.net.cn

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 中配置这些端点。spring-doc.cadn.net.cn

此外,您还可以自定义用于触发本地注销的端点,如下所示:spring-doc.cadn.net.cn

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

将本地登出与 SAML 2.0 登出分离

在某些情况下,你可能希望为本地注销暴露一个注销端点,而为RP发起的单点登出(SLO)暴露另一个注销端点。 与其他注销机制一样,只要每个端点具有不同的路径,你就可以注册多个注销端点。spring-doc.cadn.net.cn

例如,你可以像这样连接 DSL:spring-doc.cadn.net.cn

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)。spring-doc.cadn.net.cn

自定义<saml2:LogoutRequest>解决方案

通常需要在 <saml2:LogoutRequest> 中设置除 Spring Security 提供的默认值之外的其他值。spring-doc.cadn.net.cn

默认情况下,Spring Security 会发出一个 <saml2:LogoutRequest> 并提供以下内容:spring-doc.cadn.net.cn

要添加其他值,您可以使用委托,如下所示:spring-doc.cadn.net.cn

@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 中按如下方式提供自定义的 Saml2LogoutRequestResolverspring-doc.cadn.net.cn

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

自定义<saml2:LogoutResponse>解决方案

通常需要在 <saml2:LogoutResponse> 中设置除 Spring Security 提供的默认值之外的其他值。spring-doc.cadn.net.cn

默认情况下,Spring Security 会发出一个 <saml2:LogoutResponse> 并提供以下内容:spring-doc.cadn.net.cn

要添加其他值,您可以使用委托,如下所示:spring-doc.cadn.net.cn

@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 中按如下方式提供自定义的 Saml2LogoutResponseResolverspring-doc.cadn.net.cn

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

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

要自定义验证逻辑,你可以实现自己的 Saml2LogoutRequestValidator。 目前,验证逻辑非常基础,因此你可以先委托给默认的 Saml2LogoutRequestValidator,如下所示:spring-doc.cadn.net.cn

@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 中按如下方式提供自定义的 Saml2LogoutRequestValidatorspring-doc.cadn.net.cn

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

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

要自定义验证逻辑,你可以实现自己的 Saml2LogoutResponseValidator。 目前,验证逻辑非常基础,因此你可以先委托给默认的 Saml2LogoutResponseValidator,如下所示:spring-doc.cadn.net.cn

@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 中按如下方式提供自定义的 Saml2LogoutResponseValidatorspring-doc.cadn.net.cn

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

自定义<saml2:LogoutRequest>存储

当您的应用程序发送一个 <saml2:LogoutRequest> 时,该值会被存储在会话中,以便验证 RelayState 中的 InResponseTo 参数和 <saml2:LogoutResponse> 属性。spring-doc.cadn.net.cn

如果你想将注销请求存储在会话以外的其他位置,可以在 DSL 中提供你自己的实现,如下所示:spring-doc.cadn.net.cn

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