認證 <saml2:Response>s

為了驗證 SAML 2.0 Responses,Spring Security 使用 Saml2AuthenticationTokenConverter 來填充 Authentication 請求,並使用 OpenSaml4AuthenticationProvider 來對其進行認證。

您可以透過多種方式進行配置,包括

  1. 更改 RelyingPartyRegistration 的查詢方式

  2. 設定時鐘偏差以進行時間戳驗證

  3. 將響應對映到 GrantedAuthority 例項列表

  4. 定製斷言驗證策略

  5. 定製響應和斷言元素的解密策略

要配置這些,您將在 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()
}

設定時鐘偏差

斷言方和依賴方的系統時鐘不同步是很常見的。因此,您可以為 OpenSaml4AuthenticationProvider 的預設斷言驗證器配置一些容忍度

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        OpenSaml4AuthenticationProvider authenticationProvider = new OpenSaml4AuthenticationProvider();
        authenticationProvider.setAssertionValidator(OpenSaml4AuthenticationProvider
                .createDefaultAssertionValidatorWithParameters(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
                .createDefaultAssertionValidatorWithParameters(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,其中包含從 AttributeStatements 中提取的屬性以及單個 ROLE_USER 許可權。

執行額外的響應驗證

OpenSaml4AuthenticationProvider 在解密 Response 後立即驗證 IssuerDestination 值。您可以透過擴充套件預設驗證器並與您自己的響應驗證器串聯,或者完全替換為您的驗證器來定製驗證。

例如,您可以丟擲一個自定義異常,其中包含 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 斷言執行最小限度的驗證。驗證簽名後,它將

  1. 驗證 <AudienceRestriction><DelegationRestriction> 條件

  2. 驗證 <SubjectConfirmation>s,除了任何 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 透過使用在 RelyingPartyRegistration 中註冊的解密 Saml2X509Credential 例項自動解密 <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 認證。此認證管理器應接收一個包含 SAML 2.0 Response 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 {
            authorizeRequests {
                authorize(anyRequest, authenticated)
            }
            saml2Login {
                authenticationManager = customAuthenticationManager
            }
        }
        return http.build()
    }
}

使用 Saml2AuthenticatedPrincipal

為給定的斷言方正確配置依賴方後,它就可以接受斷言了。一旦依賴方驗證了斷言,結果將是一個包含 Saml2AuthenticatedPrincipalSaml2Authentication

這意味著您可以在控制器中如下訪問 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 非常方便。