執行單一登出

在其其他登出機制中,Spring Security 隨附支援 RP 和 AP 啟動的 SAML 2.0 單一登出。

簡而言之,Spring Security 支援兩種使用情境

  • RP 啟動 - 您的應用程式具有一個端點,當 POST 到該端點時,將登出使用者並將 saml2:LogoutRequest 傳送至宣告方。此後,宣告方將傳送回 saml2:LogoutResponse 並允許您的應用程式回應

  • AP 啟動 - 您的應用程式具有一個端點,將接收來自宣告方的 saml2:LogoutRequest。您的應用程式將在該點完成其登出,然後將 saml2:LogoutResponse 傳送至宣告方。

AP 啟動 情境中,您的應用程式在登出後將執行的任何本機重新導向都會變得無關緊要。一旦您的應用程式傳送 saml2:LogoutResponse,它就不再控制瀏覽器。

單一登出的最小組態

要使用 Spring Security 的 SAML 2.0 單一登出功能,您需要以下項目

  • 首先,宣告方必須支援 SAML 2.0 單一登出

  • 其次,應將宣告方組態為簽署並 POST saml2:LogoutRequestsaml2:LogoutResponse 到您應用程式的 /logout/saml2/slo 端點

  • 第三,您的應用程式必須具有 PKCS#8 私鑰和 X.509 憑證,以簽署 saml2:LogoutRequestsaml2: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 的 metadata 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 服務提供者將自動組態自身,以透過 <saml2:LogoutRequest><saml2:LogoutResponse>,使用 RP 或 AP 啟動的登出方式來協助登出。

它透過確定的啟動流程來達成此目的

  1. 查詢身份伺服器 Metadata 端點以取得 <SingleLogoutService> 元素

  2. 掃描 Metadata 並快取任何公開簽章驗證金鑰

  3. 準備適當的端點

此流程的一個後果是,身份伺服器必須啟動並接收請求,服務提供者才能成功啟動。

如果服務提供者查詢身份伺服器時,身份伺服器關閉(在適當的逾時時間內),則啟動將會失敗。

執行階段預期

在上述組態下,任何已登入的使用者都可以傳送 POST /logout 到您的應用程式,以執行 RP 啟動的 SLO。您的應用程式接著會執行以下操作

  1. 登出使用者並使 Session 失效

  2. 產生一個 <saml2:LogoutRequest> 並將其 POST 到相關宣告方的 SLO 端點

  3. 然後,如果宣告方回應一個 <saml2:LogoutResponse>,應用程式將驗證它並重新導向至已組態的成功端點

此外,當宣告方傳送一個 <saml2:LogoutRequest> 到 /logout/saml2/slo 時,您的應用程式可以參與 AP 啟動的登出。發生這種情況時,您的應用程式將執行以下操作

  1. 驗證 <saml2:LogoutRequest>

  2. 登出使用者並使 Session 失效

  3. 產生一個 <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 的 metadata 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 啟動的登出

number 1 Spring Security 執行其登出流程,呼叫其 LogoutHandler 以使 Session 失效並執行其他清理。然後,它會調用 Saml2RelyingPartyInitiatedLogoutSuccessHandler

number 2 登出成功處理器使用 Saml2LogoutRequestResolver 的實例來建立、簽署和序列化 <saml2:LogoutRequest>。它使用與目前 Saml2AuthenticatedPrincipal 相關聯的 RelyingPartyRegistration 中的金鑰和組態。然後,它會將 <saml2:LogoutRequest> 重新導向 POST 到宣告方 SLO 端點

瀏覽器將控制權交給宣告方。如果宣告方重新導向回應用程式(可能不會),則應用程式會繼續執行步驟 number 3

number 3 Saml2LogoutResponseFilter 使用其 Saml2LogoutResponseValidator 反序列化、驗證和處理 <saml2:LogoutResponse>

number 4 如果有效,則它會透過重新導向至 /login?logout 或任何已組態的內容來完成本機登出流程。如果無效,則會以 400 回應。

對於 AP 啟動的登出

number 1 Saml2LogoutRequestFilter 使用其 Saml2LogoutRequestValidator 反序列化、驗證和處理 <saml2:LogoutRequest>

number 2 如果有效,則篩選器會呼叫已組態的 LogoutHandler,使 Session 失效並執行其他清理。

number 3 它使用 Saml2LogoutResponseResolver 來建立、簽署和序列化 <saml2:LogoutResponse>。它使用從端點或 <saml2:LogoutRequest> 內容衍生的 RelyingPartyRegistration 中的金鑰和組態。然後,它會將 <saml2:LogoutResponse> 重新導向 POST 到宣告方 SLO 端點。

瀏覽器將控制權交給宣告方。

number 4 如果無效,則它會以 400 回應

組態登出端點

有三種行為可以由不同的端點觸發

  • RP 啟動的登出,允許已驗證的使用者 POST 並透過傳送 <saml2:LogoutRequest> 給宣告方來觸發登出流程

  • AP 啟動的登出,允許宣告方傳送一個 <saml2:LogoutRequest> 到應用程式

  • AP 登出回應,允許宣告方傳送一個 <saml2:LogoutResponse> 以回應 RP 啟動的 <saml2:LogoutRequest>

第一個是透過在 Principal 類型為 Saml2AuthenticatedPrincipal 時執行一般 POST /logout 來觸發。

第二個是透過 POST 到 /logout/saml2/slo 端點,並附帶由宣告方簽署的 SAMLRequest 來觸發。

第三個是透過 POST 到 /logout/saml2/slo 端點,並附帶由宣告方簽署的 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,則 Session 將被清除,但不會將 <saml2:LogoutRequest> 傳送至宣告方。但是,如果用戶端傳送 POST /saml2/logout,則應用程式將照常啟動 SAML 2.0 SLO。

自訂 <saml2:LogoutRequest> 解析

通常需要設定 <saml2:LogoutRequest> 中的其他值,而不是 Spring Security 提供的預設值。

預設情況下,Spring Security 將發出一個 <saml2:LogoutRequest> 並提供

  • Destination 屬性 - 來自 RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceLocation

  • ID 屬性 - GUID

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

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

若要新增其他值,您可以使用委派,如下所示

  • 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> 解析

通常需要設定 <saml2:LogoutResponse> 中的其他值,而不是 Spring Security 提供的預設值。

預設情況下,Spring Security 將發出一個 <saml2:LogoutResponse> 並提供

  • Destination 屬性 - 來自 RelyingPartyRegistration#getAssertingPartyDetails#getSingleLogoutServiceResponseLocation

  • ID 屬性 - GUID

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

  • <Status> 元素 - SUCCESS

若要新增其他值,您可以使用委派,如下所示

  • 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> 身份驗證

若要自訂驗證,您可以實作您自己的 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> 身份驗證

若要自訂驗證,您可以實作您自己的 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> 時,該值會儲存在 Session 中,以便驗證 <saml2:LogoutResponse> 中的 RelayState 參數和 InResponseTo 屬性。

如果您想要將登出請求儲存在 Session 以外的其他位置,您可以在 DSL 中提供您的自訂實作,如下所示

  • Java

  • Kotlin

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

更多與登出相關的參考資料