认证 <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 非常方便。