跳到主要内容

验证 <saml2:Response>

QWen Max 中英对照 SAML2 Authentication Responses Authenticating <saml2:Response>s

为了验证 SAML 2.0 响应,Spring Security 使用 Saml2AuthenticationTokenConverter 来填充 Authentication 请求,并使用 OpenSaml4AuthenticationProvider 来对其进行认证。

您可以采用多种方式进行配置,包括:

  1. 改变 RelyingPartyRegistration 的查找方式

  2. 设置时钟偏差以验证时间戳

  3. 将响应映射到 GrantedAuthority 实例列表

  4. 自定义断言验证策略

  5. 自定义响应和断言元素解密策略

要配置这些,你将在DSL中使用saml2Login#authenticationManager方法。

更改 SAML 响应处理端点

默认的端点是 /login/saml2/sso/{registrationId}。你可以在DSL和相关元数据中更改它,如下所示:

@Bean
SecurityFilterChain securityFilters(HttpSecurity http) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.loginProcessingUrl("/saml2/login/sso"))
// ...

return http.build();
}
java

和:

relyingPartyRegistrationBuilder.assertionConsumerServiceLocation("/saml/SSO")
java

更改 RelyingPartyRegistration 查找

默认情况下,此转换器将匹配任何关联的 <saml2:AuthnRequest> 或 URL 中找到的任何 registrationId。如果在这两种情况下都找不到,则尝试通过 <saml2:Response#Issuer> 元素查找。

在某些情况下,你可能需要更复杂的功能,比如支持 ARTIFACT 绑定。在这些情况下,你可以通过自定义 AuthenticationConverter 来定制查找,你可以像这样进行定制:

@Bean
SecurityFilterChain securityFilters(HttpSecurity http, AuthenticationConverter authenticationConverter) throws Exception {
http
// ...
.saml2Login((saml2) -> saml2.authenticationConverter(authenticationConverter))
// ...

return http.build();
}
java

设置时钟偏差

断言方和依赖方的系统时钟不完全同步是很常见的。因此,您可以配置 OpenSaml4AuthenticationProvider 的默认断言验证器以具有一定的容差:

@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();
}
}
java

UserDetailsService 协调

或者,你可能希望包含来自旧版 UserDetailsService 的用户详细信息。在这种情况下,响应认证转换器可能会很有用,如下所示:

@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();
}
}
java
  • 首先,调用默认的转换器,该转换器从响应中提取属性和权限

  • 其次,使用相关信息调用 UserDetailsService

  • 第三,返回一个包含用户详细信息的自定义认证

备注

调用 OpenSaml4AuthenticationProvider 的默认认证转换器不是必须的。它会返回一个包含从 AttributeStatement 中提取的属性以及单个 ROLE_USER 权限的 Saml2AuthenticatedPrincipal

执行额外的响应验证

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;
});
java

执行额外的断言验证

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

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

  2. 验证 <SubjectConfirmation>,但不包括任何 IP 地址信息

要执行额外的验证,你可以配置自己的断言验证器,该验证器委托给 OpenSaml4AuthenticationProvider 的默认验证器,然后执行其自己的验证。

例如,你可以使用 OpenSAML 的 OneTimeUseConditionValidator 来验证一个 <OneTimeUse> 条件,如下所示:

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()));
});
java
备注

虽然建议调用 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> 中的断言,你可以像这样使用它:

MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
java

如果你也在解密 <saml2:Assertion> 中的单个元素,你也可以自定义断言解密器:

provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
java
备注

由于断言可以与响应分开签名,因此有两个独立的解密器。在验证签名之前尝试解密已签名断言的元素可能会使签名无效。如果您的断言方仅对响应进行签名,则仅使用响应解密器解密所有元素是安全的。

使用自定义身份验证管理器

当然,authenticationManager DSL 方法也可以用于执行完全自定义的 SAML 2.0 身份验证。此身份验证管理器应期望一个包含 SAML 2.0 响应 XML 数据的 Saml2AuthenticationToken 对象。

@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();
}
}
java

使用 Saml2AuthenticatedPrincipal

在为特定的认证方正确配置了依赖方后,它就可以接受断言了。一旦依赖方验证了一个断言,结果将是一个包含 Saml2AuthenticatedPrincipalSaml2Authentication

这意味着你可以在控制器中像这样访问主体:

@Controller
public class MainController {
@GetMapping("/")
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
String email = principal.getFirstAttribute("email");
model.setAttribute("email", email);
return "index";
}
}
java
提示

因为 SAML 2.0 规范允许每个属性有多个值,所以你可以调用 getAttribute 来获取属性列表,或者调用 getFirstAttribute 来获取列表中的第一个属性。当你知道该属性只有一个值时,getFirstAttribute 非常方便。