对于最新的稳定版本,请使用 Spring Security 7.0.4spring-doc.cadn.net.cn

SAML 2.0 迁移

默认使用 OpenSAML 5

OpenSAML 4.x 不再受 OpenSAML 团队支持。 因此,Spring Security 将在所有情况下默认使用其 OpenSaml5 组件。spring-doc.cadn.net.cn

如果您想看看您的应用程序对此的响应如何,请执行以下操作:spring-doc.cadn.net.cn

  1. 更新您的OpenSAML依赖项至5.x版本spring-doc.cadn.net.cn

  2. 如果您正在构建一个OpenSaml4XXXSpring Security组件,请将其更改为OpenSaml5spring-doc.cadn.net.cn

如果无法选择加入,则需要添加opensaml-saml-apiopensaml-saml-impl 4.x 依赖,并从spring-security-saml2-service-provider 中排除5.x 依赖。spring-doc.cadn.net.cn

当未找到依赖方时继续过滤器链

在Spring Security 6中,Saml2WebSsoAuthenticationFilter在请求URI匹配但未找到依赖方注册时会抛出异常。spring-doc.cadn.net.cn

当应用程序不认为这是一个错误情况时,有多种情形。 例如,此过滤器不知道AuthorizationFilter对缺少依赖方将如何响应。 在某些情况下,这可能是允许的。spring-doc.cadn.net.cn

在其他情况下,您可能希望调用AuthenticationEntryPoint,如果此过滤器允许请求继续传递给AuthorizationFilter时就会发生这种情况。spring-doc.cadn.net.cn

要提高该过滤器的灵活性,在Spring Security 7中,如果未找到依赖方注册信息,则将继续过滤器链,而不是抛出异常。spring-doc.cadn.net.cn

对于许多应用程序,唯一的显著变化将是:如果依赖方注册无法找到,则会调用你的authenticationEntryPoint。 当你只有一个声明方时,默认情况下会生成一个新的认证请求并将其发送回声明方,这可能会导致“重定向过多”的循环。spring-doc.cadn.net.cn

要查看是否受到影响,您可以在Saml2WebSsoAuthenticationFilter中设置以下属性来为此变更做准备(在版本6中):spring-doc.cadn.net.cn

http
    .saml2Login((saml2) -> saml2
        .withObjectPostProcessor(new ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter>() {
			@Override
            public Saml2WebSsoAuthenticationFilter postProcess(Saml2WebSsoAuthenticationFilter filter) {
				filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true);
				return filter;
            }
        })
    )
http {
    saml2Login { }
    withObjectPostProcessor(
        object : ObjectPostProcessor<Saml2WebSsoAuhenticaionFilter?>() {
            override fun postProcess(filter: Saml2WebSsoAuthenticationFilter): Saml2WebSsoAuthenticationFilter {
            filter.setContinueChainWhenNoRelyingPartyRegistrationFound(true)
            return filter
        }
    })
}
<b:bean id="saml2PostProcessor" class="org.example.MySaml2WebSsoAuthenticationFilterBeanPostProcessor"/>

验证断言后验证响应

在Spring Security 6中,对进行身份验证的顺序如下:spring-doc.cadn.net.cn

  1. 验证响应签名(如果有的话)spring-doc.cadn.net.cn

  2. 解密响应spring-doc.cadn.net.cn

  3. 验证响应属性,例如目标和发行者spring-doc.cadn.net.cn

  4. 对于每个断言,请先验证签名、解密,然后再验证其字段spring-doc.cadn.net.cn

  5. 检查响应以确保至少有一个断言具有名称字段spring-doc.cadn.net.cn

这有时会带来挑战,因为某些响应验证在第3步进行,而另一些则在第5步进行。 具体来说,当应用程序没有名称字段并且不需要验证它时,这种情况就会带来挑战。spring-doc.cadn.net.cn

在 Spring Security 7 中,通过将响应验证移至断言验证之后并结合两个单独的验证步骤 3 和 5 而使这一过程得到了简化。 当这些步骤完成后,响应验证将不再检查 NameID 属性的存在性,并依赖于 ResponseAuthenticationConverters 来完成这一任务。spring-doc.cadn.net.cn

这将添加支持不使用ResponseAuthenticationConverter实例中的NameID元素的Authentications,并且因此不需要验证它。spring-doc.cadn.net.cn

要提前启用此行为,请使用OpenSaml5AuthenticationProvider#setValidateResponseAfterAssertions设置为true,如下所示:spring-doc.cadn.net.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
val provider = OpenSaml5AuthenticationProvider()
provider.setValidateResponseAfterAssertions(true)

这将改变认证步骤如下:spring-doc.cadn.net.cn

  1. 验证响应签名(如果有的话)spring-doc.cadn.net.cn

  2. 解密响应spring-doc.cadn.net.cn

  3. 对于每个断言,请先验证签名、解密,然后再验证其字段spring-doc.cadn.net.cn

  4. 验证响应属性,例如目标和发行者spring-doc.cadn.net.cn

注意,如果您自定义了响应认证转换器,则在需要时必须自行检查NameID元素是否存在。spring-doc.cadn.net.cn

除了更新您的响应认证转换器外,您还可以指定一个自定义的ResponseValidator,以重新添加对NameID元素的检查,如下所示:spring-doc.cadn.net.cn

OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setValidateResponseAfterAssertions(true);
ResponseValidator responseValidator = ResponseValidator.withDefaults((responseToken) -> {
	Response response = responseToken.getResponse();
	Assertion assertion = CollectionUtils.firstElement(response.getAssertions());
	Saml2Error error = new Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
            "Assertion [" + firstAssertion.getID() + "] is missing a subject");
	Saml2ResponseValidationResult failed = Saml2ResponseValidationResult.failure(error);
	if (assertion.getSubject() == null) {
		return failed;
	}
	if (assertion.getSubject().getNameID() == null) {
		return failed;
	}
	if (assertion.getSubject().getNameID().getValue() == null) {
		return failed;
	}
	return Saml2ResponseValidationResult.success();
});
provider.setResponseValidator(responseValidator);
val provider = OpenSaml5AuthenticationProvider()
provider.setValidateResponseAfterAssertions(true)
val responseValidator = ResponseValidator.withDefaults { responseToken: ResponseToken ->
	val response = responseToken.getResponse()
	val assertion = CollectionUtils.firstElement(response.getAssertions())
	val error = Saml2Error(Saml2ErrorCodes.SUBJECT_NOT_FOUND,
        "Assertion [" + firstAssertion.getID() + "] is missing a subject")
	val failed = Saml2ResponseValidationResult.failure(error)
	if (assertion.getSubject() == null) {
        return@withDefaults failed
	}
	if (assertion.getSubject().getNameID() == null) {
		return@withDefaults failed
	}
	if (assertion.getSubject().getNameID().getValue() == null) {
		return@withDefaults failed
	}
	return@withDefaults Saml2ResponseValidationResult.success()
}
provider.setResponseValidator(responseValidator)

RelyingPartyRegistration改进

RelyingPartyRegistration 将依赖方元数据与其声明方元数据链接起来。spring-doc.cadn.net.cn

为了为API的一些改进做准备,请执行以下步骤:spring-doc.cadn.net.cn

  1. 如果通过使用RelyingPartyRegistration#withRelyingPartyRegistration来修改注册信息,请改为调用RelyingPartyRegistration#mutatespring-doc.cadn.net.cn

  2. 如果提供或检索AssertingPartyDetails,请使用getAssertingPartyMetadatawithAssertingPartyMetadata方法。spring-doc.cadn.net.cn

OpenSaml5AuthenticationProvider改进

Spring Security 7 将从 OpenSaml5AuthenticationProvider 中移除少量的静态工厂方法,转而使用内部类。 这些内部类简化了对响应验证器、声明验证器和响应认证转换器的自定义。spring-doc.cadn.net.cn

响应验证

不这样做:spring-doc.cadn.net.cn

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	saml2.setResponseValidator((responseToken) -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
            .andThen((result) -> result
                .concat(myCustomValidator.convert(responseToken))
            ));
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
	saml2.setResponseValidator { responseToken -> OpenSamlAuthenticationProvider.createDefaultResponseValidator()
        .andThen { result -> result
            .concat(myCustomValidator.convert(responseToken))
        }
    }
	return saml2
}

使用 OpenSaml5AuthenticationProvider.ResponseValidator:spring-doc.cadn.net.cn

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator));
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
	saml2.setResponseValidator(ResponseValidator.withDefaults(myCustomValidator))
	return saml2
}

断言验证

不这样做:spring-doc.cadn.net.cn

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
    authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
        .createDefaultAssertionValidatorWithParameters(assertionToken -> {
            Map<String, Object> params = new HashMap<>();
            params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis());
            // ... other validation parameters
            return new ValidationContext(params);
        })
    );
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
    authenticationProvider.setAssertionValidator(OpenSaml5AuthenticationProvider
        .createDefaultAssertionValidatorWithParameters { ->
            val params = HashMap<String, Object>()
            params.put(CLOCK_SKEW, Duration.ofMinutes(10).toMillis())
            // ... other validation parameters
            return ValidationContext(params)
        }
    )
	return saml2
}

使用 OpenSaml5AuthenticationProvider.AssertionValidator:spring-doc.cadn.net.cn

@Bean
OpenSaml5AuthenticationProvider saml2AuthenticationProvider() {
	OpenSaml5AuthenticationProvider saml2 = new OpenSaml5AuthenticationProvider();
	Duration tenMinutes = Duration.ofMinutes(10);
    authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build());
	return saml2;
}
@Bean
fun saml2AuthenticationProvider(): OpenSaml5AuthenticationProvider {
	val saml2 = OpenSaml5AuthenticationProvider()
	val tenMinutes = Duration.ofMinutes(10)
    authenticationProvider.setAssertionValidator(AssertionValidator.builder().clockSkew(tenMinutes).build())
	return saml2
}

响应身份验证转换器

不这样做:spring-doc.cadn.net.cn

@Bean
Converter<ResponseToken, Saml2Authentication> authenticationConverter() {
	return (responseToken) -> {
		Saml2Authentication authentication = OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter()
            .convert(responseToken);
		// ... work with OpenSAML's Assertion object to extract the principal
		return new Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities());
	};
}
@Bean
fun authenticationConverter(): Converter<ResponseToken, Saml2Authentication> {
    return { responseToken ->
        val authentication =
            OpenSaml5AutnenticationProvider.createDefaultResponseAuthenticationConverter().convert(responseToken)
		// ... work with OpenSAML's Assertion object to extract the principal
		return Saml2Authentication(myPrincipal, authentication.getSaml2Response(), authentication.getAuthorities())
    }
}

使用 OpenSaml5AuthenticationProvider.ResponseAuthenticationConverter:spring-doc.cadn.net.cn

@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()
    authenticationConverter.setPrincipalNameConverter { assertion ->
		// ... work with OpenSAML's Assertion object to extract the principal
    }
    return authenticationConverter
}