執行單一登出
除了其他登出機制之外,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端點進行簽名和 POSTsaml2:LogoutRequest和saml2:LogoutResponse。 -
第三,您的應用程式必須有一個 PKCS#8 私鑰和 X.509 證書,用於簽名
saml2:LogoutRequest和saml2: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> 的登出。
它透過確定性啟動過程實現這一點
-
查詢身份伺服器元資料端點以獲取
<SingleLogoutService>元素。 -
掃描元資料並快取任何公共簽名驗證金鑰。
-
準備適當的端點。
此過程的一個結果是身份伺服器必須啟動並接收請求,以便服務提供商成功啟動。
| 如果服務提供商查詢身份伺服器時身份伺服器已關閉(在適當的超時情況下),則啟動將失敗。 |
執行時預期
給定上述配置,任何已登入使用者都可以向您的應用程式傳送 POST /logout 以執行 RP 發起的 SLO。您的應用程式將執行以下操作:
-
登出使用者並使會話失效。
-
生成
<saml2:LogoutRequest>並將其 POST 到關聯的斷言方的 SLO 端點。 -
然後,如果斷言方響應
<saml2:LogoutResponse>,應用程式將對其進行驗證並重定向到配置的成功端點。
此外,當斷言方將 <saml2:LogoutRequest> 傳送到 /logout/saml2/slo 時,您的應用程式可以參與 AP 發起的登出。發生這種情況時,您的應用程式將執行以下操作:
-
驗證
<saml2:LogoutRequest>。 -
登出使用者並使會話失效。
-
生成
<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 發起的登出
Spring Security 執行其登出流程,呼叫其 LogoutHandler 以使會話失效並執行其他清理。然後,它呼叫 Saml2RelyingPartyInitiatedLogoutSuccessHandler。
登出成功處理程式使用 Saml2LogoutRequestResolver 例項來建立、簽名和序列化 <saml2:LogoutRequest>。它使用與當前 Saml2AuthenticatedPrincipal 相關聯的 RelyingPartyRegistration 中的金鑰和配置。然後,它透過重定向 POST 將 <saml2:LogoutRequest> 傳送到斷言方 SLO 端點。
瀏覽器將控制權交給斷言方。如果斷言方重定向回來(可能不會),則應用程式繼續執行步驟
。
Saml2LogoutResponseFilter 使用其 Saml2LogoutResponseValidator 反序列化、驗證和處理 <saml2:LogoutResponse>。
如果有效,則透過重定向到 /login?logout 或已配置的任何內容來完成本地登出流程。如果無效,則響應 400。
對於 AP 發起的登出
Saml2LogoutRequestFilter 使用其 Saml2LogoutRequestValidator 反序列化、驗證和處理 <saml2:LogoutRequest>。
如果有效,則過濾器呼叫已配置的 LogoutHandler,使會話失效並執行其他清理。
它使用 Saml2LogoutResponseResolver 來建立、簽名和序列化 <saml2:LogoutResponse>。它使用從端點或從 <saml2:LogoutRequest> 的內容派生的 RelyingPartyRegistration 中的金鑰和配置。然後,它透過重定向 POST 將 <saml2:LogoutResponse> 傳送到斷言方 SLO 端點。
瀏覽器將控制權交給斷言方。
如果無效,則它響應 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
}
}
}
進一步的登出相關參考
-
CSRF 注意事項部分中的登出