认证 <saml2:Response>

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

为了验证 SAML 2.0 响应,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 查询

默认情况下,该 converter 将匹配任何关联的 <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
                .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 中包含用户细节(user details)。在这种情况下,响应认证转换器(authentication converter)可以派上用场,如下图所示。

  • 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 首先,调用默认转换器,它从响应中提取属性(attributes )和授权(authorities )。
2 第二,使用相关信息调用 UserDetailsService
3 第三,返回一个包括用户详细信息的自定义认证。
它不需要调用 OpenSaml4AuthenticationProvider 的默认认证转换器。它返回一个 Saml2AuthenticatedPrincipal,包含它从 AttributeStatements 中提取的属性(attributes)以及单一的 ROLE_USER 权限。

进行额外的响应验证(Response Validation)

OpenSaml4AuthenticationProvider 在解密响应后立即验 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;
});

进行额外的断言验证(Assertion Validation)

OpenSaml4AuthenticationProvider 对SAML 2.0断言执行最小的验证。在验证了签名后,它将:

  1. 验证 <AudienceRestriction><DelegationRestriction> 条件。

  2. 验证 <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 通过使用在 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) }
有两个单独的解密器,因为断言可以与响应分开签署。试图在签名验证之前解密一个已签名的断言的元素可能会使签名无效。如果你的断言方只签署了响应,那么只使用响应解密器来解密所有元素是安全的。

使用自定义认证管理器(Authentication Manager)

当然,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 是非常方便的。