驗證 <saml2:Response>
為了驗證 SAML 2.0 響應,Spring Security 使用 Saml2AuthenticationTokenConverter 來填充 Authentication 請求,並使用 OpenSaml5AuthenticationProvider 來驗證它。
您可以透過多種方式進行配置,包括:
-
更改
RelyingPartyRegistration的查詢方式 -
為時間戳驗證設定時鐘偏移
-
將響應對映到
GrantedAuthority例項列表 -
自定義斷言的驗證策略
-
自定義解密響應和斷言元素的策略
要配置這些,您將在 DSL 中使用 saml2Login#authenticationManager 方法。
更改 SAML 響應處理端點
預設端點是 /login/saml2/sso/{registrationId}。您可以在 DSL 和關聯的元資料中更改它,如下所示:
-
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 繫結。在這些情況下,您可以透過自定義 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()
}
設定時鐘偏移
斷言方和依賴方的系統時鐘未完全同步是很常見的。因此,您可以配置 OpenSaml5AuthenticationProvider.AssertionValidator,如下所示:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
AssertionValidator assertionValidator = AssertionValidator.builder()
.clockSkew(Duration.ofMinutes(10)).build();
authenticationProvider.setAssertionValidator(assertionValidator);
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.saml2Login((saml2) -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
return http.build();
}
}
@Configuration @EnableWebSecurity
class SecurityConfig {
@Bean
@Throws(Exception::class)
fun filterChain(http: HttpSecurity): SecurityFilterChain {
val authenticationProvider = OpenSaml5AuthenticationProvider()
val assertionValidator = AssertionValidator.builder().clockSkew(Duration.ofMinutes(10)).build()
authenticationProvider.setAssertionValidator(assertionValidator)
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
return http.build()
}
}
將 Assertion 轉換為 Authentication
OpenSamlXAuthenticationProvider#setResponseAuthenticationConverter 提供了一種方法,讓您更改它如何將您的斷言轉換為 Authentication 例項。
您可以透過以下方式設定自定義轉換器:
-
Java
-
Kotlin
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
Converter<ResponseToken, Saml2Authentication> authenticationConverter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
OpenSaml5AuthenticationProvider authenticationProvider = new OpenSaml5AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter);
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated())
.saml2Login((saml2) -> saml2
.authenticationManager(new ProviderManager(authenticationProvider))
);
return http.build();
}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {
@Autowired
var authenticationConverter: Converter<ResponseToken, Saml2Authentication>? = null
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
val authenticationProvider = OpenSaml5AuthenticationProvider()
authenticationProvider.setResponseAuthenticationConverter(this.authenticationConverter)
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = ProviderManager(authenticationProvider)
}
}
return http.build()
}
}
接下來的示例都建立在此常見構造之上,以向您展示此轉換器在不同情況下如何派上用場。
與 UserDetailsService 協調
或者,您可能希望包含來自傳統 UserDetailsService 的使用者詳細資訊。在這種情況下,響應身份驗證轉換器會派上用場,如下所示:
-
Java
-
Kotlin
@Component
class MyUserDetailsResponseAuthenticationConverter implements Converter<ResponseToken, Saml2Authentication> {
private final ResponseAuthenticationConverter delegate = new ResponseAuthenticationConverter();
private final UserDetailsService userDetailsService;
MyUserDetailsResponseAuthenticationConverter(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
Saml2Authentication authentication = this.delegate.convert(responseToken); (1)
UserDetails principal = this.userDetailsService.loadByUsername(username); (2)
String saml2Response = authentication.getSaml2Response();
Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
saml2Response, CollectionUtils.getFirst(response.getAssertions()));
Collection<GrantedAuthority> authorities = principal.getAuthorities();
return new Saml2AssertionAuthentication(userDetails, assertion, authorities); (3)
}
}
@Component
open class MyUserDetailsResponseAuthenticationConverter(val delegate: ResponseAuthenticationConverter,
UserDetailsService userDetailsService): Converter<ResponseToken, Saml2Authentication> {
@Override
open fun convert(responseToken: ResponseToken): Saml2Authentication {
val authentication = this.delegate.convert(responseToken) (1)
val principal = this.userDetailsService.loadByUsername(username) (2)
val saml2Response = authentication.getSaml2Response()
val assertion = OpenSamlResponseAssertionAccessor(
saml2Response, CollectionUtils.getFirst(response.getAssertions()))
val authorities = principal.getAuthorities()
return Saml2AssertionAuthentication(userDetails, assertion, authorities) (3)
}
}
| 1 | 首先,呼叫預設轉換器,它從響應中提取屬性和許可權 |
| 2 | 其次,使用相關資訊呼叫 UserDetailsService |
| 3 | 第三,返回包含使用者詳細資訊的身份驗證 |
|
如果您的 |
不需要呼叫 OpenSaml5AuthenticationProvider 的預設身份驗證轉換器。它返回一個 Saml2AuthenticatedPrincipal,其中包含從 AttributeStatement 中提取的屬性以及單個 ROLE_USER 許可權。 |
配置主體名稱
有時,主體名稱不在 <saml2:NameID> 元素中。在這種情況下,您可以使用自定義策略配置 ResponseAuthenticationConverter,如下所示:
-
Java
-
Kotlin
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... work with OpenSAML's Assertion object to extract the principal
});
return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
val authenticationConverter: ResponseAuthenticationConverter = ResponseAuthenticationConverter()
authenticationConverter.setPrincipalNameConverter { assertion ->
// ... work with OpenSAML's Assertion object to extract the principal
}
return authenticationConverter
}
配置主體的授予許可權
使用 OpenSamlXAuhenticationProvider 時,Spring Security 會自動授予 ROLE_USER。使用 OpenSaml5AuthenticationProvider,您可以配置一組不同的授予許可權,如下所示:
-
Java
-
Kotlin
@Bean
ResponseAuthenticationConverter authenticationConverter() {
ResponseAuthenticationConverter authenticationConverter = new ResponseAuthenticationConverter();
authenticationConverter.setPrincipalNameConverter((assertion) -> {
// ... grant the needed authorities based on attributes in the assertion
});
return authenticationConverter;
}
@Bean
fun authenticationConverter(): ResponseAuthenticationConverter {
val authenticationConverter = ResponseAuthenticationConverter()
authenticationConverter.setPrincipalNameConverter{ assertion ->
// ... grant the needed authorities based on attributes in the assertion
}
return authenticationConverter
}
執行額外的響應驗證
OpenSaml5AuthenticationProvider 在解密 Response 後立即驗證 Issuer 和 Destination 值。您可以透過擴充套件預設驗證器並與您自己的響應驗證器連線來定製驗證,或者您可以完全替換為您的驗證器。
例如,您可以丟擲一個自定義異常,其中包含 Response 物件中可用的任何額外資訊,如下所示:
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = ResponseValidator.withDefaults(myCustomValidator);
provider.setResponseValidator(responseValidator);
您還可以自定義 Spring Security 應該執行的驗證步驟。例如,如果您想跳過 Response#InResponseTo 驗證,您可以呼叫 ResponseValidator 的建構函式,將 InResponseToValidator 從列表中排除。
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
ResponseValidator responseValidator = new ResponseValidator(new DestinationValidator(), new IssuerValidator());
provider.setResponseValidator(responseValidator);
|
OpenSAML 在其 |
執行額外的斷言驗證
OpenSaml5AuthenticationProvider 對 SAML 2.0 斷言執行最小驗證。驗證簽名後,它將:
-
驗證
<AudienceRestriction>和<DelegationRestriction>條件 -
驗證
<SubjectConfirmation>,除了任何 IP 地址資訊
要執行額外的驗證,您可以配置自己的斷言驗證器,該驗證器委託給 OpenSaml5AuthenticationProvider 的預設驗證器,然後執行自己的驗證。
例如,您可以使用 OpenSAML 的 OneTimeUseConditionValidator 來驗證 <OneTimeUse> 條件,如下所示:
-
Java
-
Kotlin
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
OneTimeUseConditionValidator validator = ...;
AssertionValidator assertionValidator = AssertionValidator.builder()
.conditionValidators((c) -> c.add(validator)).build();
provider.setAssertionValidator(assertionValidator);
val provider = OpenSaml5AuthenticationProvider()
val validator: OneTimeUseConditionValidator = ...;
val assertionValidator = AssertionValidator.builder()
.conditionValidators { add(validator) }.build()
provider.setAssertionValidator(assertionValidator)
您可以使用此相同的構建器來刪除您不想使用的驗證器,如下所示:
-
Java
-
Kotlin
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
AssertionValidator assertionValidator = AssertionValidator.builder()
.conditionValidators((c) -> c.removeIf(AudienceRestrictionValidator.class::isInstance)).build();
provider.setAssertionValidator(assertionValidator);
val provider = new OpenSaml5AuthenticationProvider()
val assertionValidator = AssertionValidator.builder()
.conditionValidators {
c: List<ConditionValidator> -> c.removeIf { it is AudienceRestrictionValidator }
}.build()
provider.setAssertionValidator(assertionValidator)
自定義解密
Spring Security 透過使用在 RelyingPartyRegistration 中註冊的解密 Saml2X509Credential 例項,自動解密 <saml2:EncryptedAssertion>、<saml2:EncryptedAttribute> 和 <saml2:EncryptedID> 元素。
OpenSaml5AuthenticationProvider 暴露了 兩種解密策略。響應解密器用於解密 <saml2:Response> 的加密元素,例如 <saml2:EncryptedAssertion>。斷言解密器用於解密 <saml2:Assertion> 的加密元素,例如 <saml2:EncryptedAttribute> 和 <saml2:EncryptedID>。
您可以將 OpenSaml5AuthenticationProvider 的預設解密策略替換為您自己的策略。例如,如果您有一個單獨的服務來解密 <saml2:Response> 中的斷言,您可以改用它,如下所示:
-
Java
-
Kotlin
MyDecryptionService decryptionService = ...;
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml5AuthenticationProvider()
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 身份驗證。此身份驗證管理器應期望一個包含 SAML 2.0 響應 XML 資料的 Saml2AuthenticationToken 物件。
-
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 {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = customAuthenticationManager
}
}
return http.build()
}
}
使用 Saml2AuthenticatedPrincipal
當依賴方為給定的斷言方正確配置後,它就可以接受斷言了。一旦依賴方驗證了斷言,結果將是帶有 Saml2AuthenticatedPrincipal 的 Saml2Authentication。
這意味著您可以在控制器中訪問主體,如下所示:
-
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 非常方便。 |