執行單點登出
除了其其他登出機制外,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:LogoutRequest
和saml2:LogoutResponse
到您的應用的/logout/saml2/slo
端點。 -
第三,您的應用必須擁有 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>
響應,應用將驗證它並重定向到配置的成功端點。
此外,當宣告方向 /logout/saml2/slo
傳送 <saml2:LogoutRequest>
時,您的應用可以參與 AP 發起的登出。發生這種情況時,您的應用將執行以下操作:
-
驗證
<saml2:LogoutRequest>
。 -
登出使用者並使會話失效。
-
生成一個
<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 的元資料 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
中的金鑰和配置。然後,它將 <saml2:LogoutRequest>
透過重定向的方式 POST 到宣告方的 SLO 端點。
瀏覽器將控制權交給宣告方。如果宣告方重定向回來(它可能不會),則應用繼續執行步驟 。
Saml2LogoutResponseFilter
使用其 Saml2LogoutResponseValidator
反序列化、驗證和處理 <saml2:LogoutResponse>
。
如果有效,則它透過重定向到
/login?logout
或任何已配置的地址來完成本地登出流程。如果無效,則響應 400。
對於 AP 發起的登出:
Saml2LogoutRequestFilter
使用其 Saml2LogoutRequestValidator
反序列化、驗證和處理 <saml2:LogoutRequest>
。
如果有效,則過濾器呼叫配置的
LogoutHandler
,使會話失效並執行其他清理工作。
它使用
Saml2LogoutResponseResolver
來建立、簽名和序列化一個 <saml2:LogoutResponse>
。它使用從端點或 <saml2:LogoutRequest>
內容派生的 RelyingPartyRegistration
中的金鑰和配置。然後,它將 <saml2:LogoutResponse>
透過重定向的方式 POST 到宣告方的 SLO 端點。
瀏覽器將控制權交給宣告方。
如果無效,則它響應 400。
配置登出端點
可以透過不同的端點觸發三種行為:
-
RP 發起的登出,它允許已認證使用者傳送
POST
請求,並透過向宣告方傳送<saml2:LogoutRequest>
來觸發登出過程。 -
AP 發起的登出,它允許宣告方向應用傳送
<saml2:LogoutRequest>
。 -
AP 登出響應,它允許宣告方響應 RP 發起的
<saml2:LogoutRequest>
,傳送一個<saml2:LogoutResponse>
。
第一種行為在主體型別為 Saml2AuthenticatedPrincipal
時,透過執行正常的 POST /logout
來觸發。
第二種行為是透過 POST 一個由宣告方簽名的 SAMLRequest
到 /logout/saml2/slo
端點來觸發。
第三種行為是透過 POST 一個由宣告方簽名的 SAMLResponse
到 /logout/saml2/slo
端點來觸發。
由於使用者已經登入或原始登出請求已知,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>
並提供:
-
Destination
屬性 - 來自RelyingPartyRegistration#getAssertingPartyMetadata#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#getAssertingPartyMetadata#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 = OpenSaml4LogoutResponseValidator()
@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>
時,該值會儲存在會話中,以便可以驗證 <saml2:LogoutResponse>
中的 RelayState
引數和 InResponseTo
屬性。
如果您想將登出請求儲存在會話之外的其他位置,您可以在 DSL 中提供您的自定義實現,如下所示:
-
Java
-
Kotlin
http
.saml2Logout((saml2) -> saml2
.logoutRequest((request) -> request
.logoutRequestRepository(myCustomLogoutRequestRepository)
)
);
http {
saml2Logout {
logoutRequest {
logoutRequestRepository = myCustomLogoutRequestRepository
}
}
}
更多登出相關參考
-
CSRF 注意事項中的登出