執行單一登出

除了其他登出機制之外,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 單一登出。

  • 其次,斷言方應配置為對您的應用程式的 /logout/saml2/slo 端點進行簽名和 POST saml2:LogoutRequestsaml2:LogoutResponse

  • 第三,您的應用程式必須有一個 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 的元資料 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 服務提供商將自動配置自身,以透過 RP 或 AP 發起的登出來促進 <saml2:LogoutRequest><saml2:LogoutResponse> 的登出。

它透過確定性啟動過程實現這一點

  1. 查詢身份伺服器元資料端點以獲取 <SingleLogoutService> 元素。

  2. 掃描元資料並快取任何公共簽名驗證金鑰。

  3. 準備適當的端點。

此過程的一個結果是身份伺服器必須啟動並接收請求,以便服務提供商成功啟動。

如果服務提供商查詢身份伺服器時身份伺服器已關閉(在適當的超時情況下),則啟動將失敗。

執行時預期

給定上述配置,任何已登入使用者都可以向您的應用程式傳送 POST /logout 以執行 RP 發起的 SLO。您的應用程式將執行以下操作:

  1. 登出使用者並使會話失效。

  2. 生成 <saml2:LogoutRequest> 並將其 POST 到關聯的斷言方的 SLO 端點。

  3. 然後,如果斷言方響應 <saml2:LogoutResponse>,應用程式將對其進行驗證並重定向到配置的成功端點。

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

  1. 驗證 <saml2:LogoutRequest>

  2. 登出使用者並使會話失效。

  3. 生成 <saml2:LogoutResponse> 並將其 POST 回斷言方的 SLO 端點。

沒有 Boot 的最小配置

您也可以透過直接釋出 bean 來實現相同的結果,而不是使用 Boot 屬性,如下所示:

  • 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 - 用於簽名 <saml2:LogoutRequest><saml2:LogoutResponse> 的簽名憑據,您也可以將其新增到多個信賴方
4 - 其次,指示您的應用程式希望使用 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 中的金鑰和配置。然後,它透過重定向 POST 將 <saml2:LogoutRequest> 傳送到斷言方 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 中的金鑰和配置。然後,它透過重定向 POST 將 <saml2:LogoutResponse> 傳送到斷言方 SLO 端點。

瀏覽器將控制權交給斷言方。

數字 4 如果無效,則它響應 400

配置登出端點

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

  • RP 發起的登出,它允許已認證的使用者 POST 並透過向斷言方傳送 <saml2:LogoutRequest> 來觸發登出過程。

  • 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 中配置過濾器,如下所示:

  • 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,會話將被清除,但不會向斷言方傳送 <saml2:LogoutRequest>。但是,如果客戶端傳送 POST /saml2/logout,則應用程式將正常啟動 SAML 2.0 SLO。

自定義 <saml2:LogoutRequest> 解析

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

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

  • DestinationValidator 屬性 - 來自 RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceLocation

  • ID 屬性 - 一個 GUID

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

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

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

  • Java

  • Kotlin

@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 中提供您的自定義 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> 並提供

  • DestinationValidator 屬性 - 來自 RelyingPartyRegistration#getAssertingPartyMetadata#getSingleLogoutServiceResponseLocation

  • ID 屬性 - 一個 GUID

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

  • <Status> 元素 - SUCCESS

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

  • Java

  • Kotlin

@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 中提供您的自定義 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 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 中提供您的自定義 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 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 中提供您的自定義 Saml2LogoutResponseValidator,如下所示:

  • Java

  • Kotlin

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

自定義 <saml2:LogoutRequest> 儲存

當您的應用程式傳送 <saml2:LogoutRequest> 時,該值會儲存在會話中,以便可以驗證 RelayState 引數和 <saml2:LogoutResponse> 中的 InResponseTo 屬性。

如果您希望將登出請求儲存在會話之外的其他位置,您可以在 DSL 中提供您的自定義實現,如下所示:

  • Java

  • Kotlin

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

進一步的登出相關參考

© . This site is unofficial and not affiliated with VMware.