此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.5! |
认证<saml2:Response>
s
为了验证 SAML 2.0 响应,Spring Security 使用Saml2AuthenticationTokenConverter
要填充Authentication
request 和OpenSaml4AuthenticationProvider
对其进行身份验证。
您可以通过多种方式进行配置,包括:
-
改变
RelyingPartyRegistration
已查找 -
将 clock skew 设置为 timestamp 验证
-
将响应映射到
GrantedAuthority
实例 -
自定义验证断言的策略
-
自定义解密响应和断言元素的策略
要配置这些参数,您将使用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>
或任何registrationId
它在 URL 中找到。
或者,如果它在这两种情况下都找不到,那么它会尝试通过<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()
}
设置 clock skew
断言方和依赖方的系统时钟不完全同步的情况并不少见。
因此,您可以配置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()
}
}
如果您使用的是 OpenSAML 5,那么我们有一个更简单的方法,使用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(authz -> authz
.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((authz) -> authz
.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 {
authorizeRequests {
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();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
return new Saml2Authentication((AuthenticatedPrincipal) userDetails, saml2Response, 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 authorities = principal.getAuthorities()
return Saml2Authentication(userDetails as AuthenticatedPrincipal, saml2Response, authorities) (3)
}
}
1 | 首先,调用默认转换器,该转换器从响应中提取属性和权限 |
2 | 其次,调用UserDetailsService 使用相关信息 |
3 | 第三,返回包含用户详细信息的身份验证 |
如果您的 |
或者,如果您使用的是 OpenSaml 4,则可以实现如下类似功能:
-
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 包含它从中提取的属性AttributeStatement s 以及单个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
}
配置委托人的已授予权限
Spring Security 自动授予ROLE_USER
使用OpenSamlXAuhenticationProvider
.
跟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
}
执行其他响应验证
OpenSaml4AuthenticationProvider
验证Issuer
和Destination
值Response
.
您可以通过扩展默认验证器与您自己的响应验证器连接来自定义验证,也可以将其完全替换为您的验证器。
例如,您可以引发自定义异常,其中包含Response
object,如下所示:
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;
});
使用OpenSaml5AuthenticationProvider
,您可以使用更少的样板执行相同的作:
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 执行 |
执行其他断言验证
OpenSaml4AuthenticationProvider
对 SAML 2.0 断言执行最小验证。
验证签名后,它将:
-
驗證
<AudienceRestriction>
和<DelegationRestriction>
条件 -
驗證
<SubjectConfirmation>
s,需要任何 IP 地址信息
要执行其他验证,您可以配置自己的断言验证器,该验证器将OpenSaml4AuthenticationProvider
的 default,然后执行自己的
例如,您可以使用 OpenSAML 的OneTimeUseConditionValidator
要同时验证<OneTimeUse>
condition,如下所示:
-
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> 因为你自己在做这些。 |
如果您使用的是 OpenSAML 5,那么我们有一种更简单的方法,使用OpenSaml5AuthenticationProvider.AssertionValidator
:
-
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 解密<saml2:EncryptedAssertion>
,<saml2:EncryptedAttribute>
和<saml2:EncryptedID>
元素Saml2X509Credential
实例在RelyingPartyRegistration
.
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 身份验证。
此身份验证管理器应期望Saml2AuthenticationToken
包含 SAML 2.0 响应 XML 数据的对象。
-
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
为给定断言方正确配置信赖方后,即可接受断言。
一旦依赖方验证了断言,结果就会出现Saml2Authentication
替换为Saml2AuthenticatedPrincipal
.
这意味着您可以像这样访问控制器中的委托人:
-
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 当您知道只有一个值时,它非常方便。 |