驗證 <saml2:Response>
為了驗證 SAML 2.0 回應,Spring Security 使用 Saml2AuthenticationTokenConverter
來填充 Authentication
請求,並使用 OpenSaml4AuthenticationProvider
來驗證它。
您可以透過多種方式設定此功能,包括
-
變更
RelyingPartyRegistration
的查找方式 -
設定時鐘偏移以進行時間戳記驗證
-
將回應映射到
GrantedAuthority
實例的列表 -
自訂驗證斷言的策略
-
自訂解密回應和斷言元素的策略
若要設定這些,您將在 DSL 中使用 saml2Login#authenticationManager
方法。
變更 SAML 回應處理端點
預設端點為 /login/saml2/sso/{registrationId}
。您可以在 DSL 和相關聯的 metadata 中變更此設定,如下所示
-
Java
-
Kotlin
@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
// ...
return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity): SecurityFilterChain {
http {
// ...
.saml2Login {
loginProcessingUrl = "/saml2/login/sso"
}
// ...
}
return http.build()
}
以及
-
Java
-
Kotlin
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
變更 RelyingPartyRegistration
查找
預設情況下,此轉換器將比對任何相關聯的 <saml2:AuthnRequest>
或在 URL 中找到的任何 registrationId
。或者,如果在這些情況下都找不到,則它會嘗試透過 <saml2:Response#Issuer>
元素來查找。
在許多情況下,您可能需要更複雜的功能,例如,如果您支援 ARTIFACT
binding。在這些情況下,您可以透過自訂 AuthenticationConverter
自訂查找,您可以像這樣自訂它
-
Java
-
Kotlin
@Bean
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
// ...
return http.build();
}
@Bean
fun securityFilters(val http: HttpSecurity, val converter: AuthenticationConverter): SecurityFilterChain {
http {
// ...
.saml2Login {
authenticationConverter = converter
}
// ...
}
return http.build()
}
設定時鐘偏移
聲明方和依賴方的系統時鐘未完全同步的情況並不少見。因此,您可以為 OpenSaml4AuthenticationProvider
的預設斷言驗證器設定一些容 tolerance
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator(assertionToken -> {
Map<String, Object> params = new HashMap<>();
params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
// ... other validation parameters
return new ValidationContext(params);
})
);
http
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val authenticationProvider = OpenSaml4AuthenticationProvider()
authenticationProvider.setAssertionValidator(
OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator(Converter<OpenSaml4AuthenticationProvider.AssertionToken, ValidationContext> {
val params: MutableMap<String, Any> = HashMap()
params[CLOCK_SKEW] =
Duration.ofMinutes(10).toMillis()
ValidationContext(params)
})
)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
return http.build()
}
}
與 UserDetailsService
協調
或者,您可能想要包含來自舊版 UserDetailsService
的使用者詳細資訊。在這種情況下,回應身份驗證轉換器可以派上用場,如下所示
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter() (1)
.convert(responseToken);
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
String username = assertion.getSubject().getNameID().getValue();
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); (2)
return MySaml2Authentication(userDetails, authentication); (3)
});
http
.authorizeHttpRequests(authz -> authz
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Autowired
var userDetailsService: UserDetailsService? = null
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val authenticationProvider = OpenSaml4AuthenticationProvider()
authenticationProvider.setResponseAuthenticationConverter { responseToken: OpenSaml4AuthenticationProvider.ResponseToken ->
val authentication = OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter() (1)
.convert(responseToken)
val assertion: Assertion = responseToken.response.assertions[0]
val username: String = assertion.subject.nameID.value
val userDetails = userDetailsService!!.loadUserByUsername(username) (2)
MySaml2Authentication(userDetails, authentication) (3)
}
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
return http.build()
}
}
1 | 首先,呼叫預設轉換器,它從回應中提取屬性和授權 |
2 | 其次,使用相關資訊呼叫 UserDetailsService |
3 | 第三,傳回包含使用者詳細資訊的自訂身份驗證 |
不需要呼叫 OpenSaml4AuthenticationProvider 的預設身份驗證轉換器。它會傳回一個 Saml2AuthenticatedPrincipal ,其中包含從 AttributeStatement 提取的屬性以及單一 ROLE_USER 授權。 |
執行額外的回應驗證
OpenSaml4AuthenticationProvider
在解密 Response
後立即驗證 Issuer
和 Destination
值。您可以透過擴展預設驗證器並與您自己的回應驗證器串聯,來自訂驗證,或者您可以完全使用您的驗證器來取代它。
例如,您可以拋出自訂例外,其中包含 Response
物件中提供的任何其他資訊,如下所示
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseValidator((responseToken) -> {
Saml2ResponseValidatorResult result = OpenSamlAuthenticationProvider
.createDefaultResponseValidator()
.convert(responseToken)
.concat(myCustomValidator.convert(responseToken));
if (!result.getErrors().isEmpty()) {
String inResponseTo = responseToken.getInResponseTo();
throw new CustomSaml2AuthenticationException(result, inResponseTo);
}
return result;
});
執行額外的斷言驗證
OpenSaml4AuthenticationProvider
對 SAML 2.0 斷言執行最小驗證。在驗證簽章後,它將
-
驗證
<AudienceRestriction>
和<DelegationRestriction>
條件 -
驗證
<SubjectConfirmation>
,除了任何 IP 位址資訊外
若要執行額外的驗證,您可以設定自己的斷言驗證器,該驗證器委派給 OpenSaml4AuthenticationProvider
的預設驗證器,然後執行自己的驗證。
例如,您可以使用 OpenSAML 的 OneTimeUseConditionValidator
也驗證 <OneTimeUse>
條件,如下所示
-
Java
-
Kotlin
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
provider.setAssertionValidator(assertionToken -> {
Saml2ResponseValidatorResult result = OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator()
.convert(assertionToken);
Assertion assertion = assertionToken.getAssertion();
OneTimeUse oneTimeUse = assertion.getConditions().getOneTimeUse();
ValidationContext context = new ValidationContext();
try {
if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
return result;
}
} catch (Exception e) {
return result.concat(new Saml2Error(INVALID_ASSERTION, e.getMessage()));
}
return result.concat(new Saml2Error(INVALID_ASSERTION, context.getValidationFailureMessage()));
});
var provider = OpenSaml4AuthenticationProvider()
var validator: OneTimeUseConditionValidator = ...
provider.setAssertionValidator { assertionToken ->
val result = OpenSaml4AuthenticationProvider
.createDefaultAssertionValidator()
.convert(assertionToken)
val assertion: Assertion = assertionToken.assertion
val oneTimeUse: OneTimeUse = assertion.conditions.oneTimeUse
val context = ValidationContext()
try {
if (validator.validate(oneTimeUse, assertion, context) = ValidationResult.VALID) {
return@setAssertionValidator result
}
} catch (e: Exception) {
return@setAssertionValidator result.concat(Saml2Error(INVALID_ASSERTION, e.message))
}
result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
雖然建議使用,但沒有必要呼叫 OpenSaml4AuthenticationProvider 的預設斷言驗證器。如果您不需要它檢查 <AudienceRestriction> 或 <SubjectConfirmation> ,因為您自己正在執行這些檢查,則可以跳過它。 |
自訂解密
Spring Security 會使用在 Saml2X509Credential
實例 中註冊的解密 RelyingPartyRegistration
,自動解密 <saml2:EncryptedAssertion>
、<saml2:EncryptedAttribute>
和 <saml2:EncryptedID>
元素。
OpenSaml4AuthenticationProvider
公開了 兩種解密策略。回應解密器用於解密 <saml2:Response>
的加密元素,例如 <saml2:EncryptedAssertion>
。斷言解密器用於解密 <saml2:Assertion>
的加密元素,例如 <saml2:EncryptedAttribute>
和 <saml2:EncryptedID>
。
您可以將 OpenSaml4AuthenticationProvider
的預設解密策略替換為您自己的策略。例如,如果您有另一個服務來解密 <saml2:Response>
中的斷言,您可以改用它,如下所示
-
Java
-
Kotlin
MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
如果您也解密 <saml2:Assertion>
中的個別元素,您也可以自訂斷言解密器
-
Java
-
Kotlin
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
有兩個單獨的解密器,因為斷言可以與回應分開簽署。在簽章驗證之前嘗試解密已簽署的斷言元素可能會使簽章失效。如果您的聲明方僅簽署回應,則僅使用回應解密器解密所有元素是安全的。 |
使用自訂驗證管理器
當然,authenticationManager
DSL 方法也可以用於執行完全自訂的 SAML 2.0 身份驗證。此身份驗證管理器應預期一個 Saml2AuthenticationToken
物件,其中包含 SAML 2.0 回應 XML 資料。
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(authenticationManager)
)
;
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = customAuthenticationManager
}
}
return http.build()
}
}
使用 Saml2AuthenticatedPrincipal
在為給定的聲明方正確設定依賴方後,它就可以接受斷言了。一旦依賴方驗證了斷言,結果將是一個具有 Saml2AuthenticatedPrincipal
的 Saml2Authentication
。
這表示您可以在您的控制器中像這樣存取 principal
-
Java
-
Kotlin
@Controller
public class MainController {
@GetMapping("/")
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
String email = principal.getFirstAttribute("email");
model.setAttribute("email", email);
return "index";
}
}
@Controller
class MainController {
@GetMapping("/")
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
val email = principal.getFirstAttribute<String>("email")
model.setAttribute("email", email)
return "index"
}
}
由於 SAML 2.0 規範允許每個屬性具有多個值,因此您可以呼叫 getAttribute 來取得屬性列表,或呼叫 getFirstAttribute 來取得列表中的第一個。當您知道只有一個值時,getFirstAttribute 非常方便。 |