验证 <saml2:Response>
<saml2:Response>s
为了验证SAML 2.0响应,Spring Security使用Saml2AuthenticationTokenConverter来填充Authentication请求,并使用OpenSaml5AuthenticationProvider来对其进行认证。
您可以通过多种方式进行配置,包括:
-
更改
RelyingPartyRegistration的查找方式 -
为时间戳验证设置时钟偏移量
-
将响应映射到
GrantedAuthority实例列表 -
自定义断言验证策略
-
自定义响应和断言元素的解密策略
要配置这些,你需要在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()
}
设置时钟偏移
断言方和依赖方的系统时钟不完全同步的情况并不少见。因此,您可以按如下方式配置 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((authorize) -> authorize
.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 提供了一种方式,允许您自定义如何将断言(assertion)转换为 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((authorize) -> authorize
.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 {
authorizeHttpRequests {
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();
Saml2ResponseAssertionAccessor assertion = new OpenSamlResponseAssertionAccessor(
saml2Response, CollectionUtils.getFirst(response.getAssertions()));
Collection<GrantedAuthority> authorities = principal.getAuthorities();
return new Saml2AssertionAuthentication(userDetails, assertion, 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 assertion = OpenSamlResponseAssertionAccessor(
saml2Response, CollectionUtils.getFirst(response.getAssertions()))
val authorities = principal.getAuthorities()
return Saml2AssertionAuthentication(userDetails, assertion, authorities) 3
}
}
首先,调用默认转换器,从响应中提取属性和权限
其次,使用相关信息调用 UserDetailsService
最后,返回包含用户详情的认证信息
如果你的 UserDetailsService 返回的值也实现了 AuthenticatedPrincipal 接口,那么你就不需要自定义认证实现。
无需调用 OpenSaml5AuthenticationProvider 的默认认证转换器。它会返回一个 Saml2AuthenticatedPrincipal,其中包含从 AttributeStatement 中提取的属性以及单一的 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
}
配置主体的授予权限
使用 OpenSamlXAuthenticationProvider 时,Spring Security 会自动授予 ROLE_USER 角色。而使用 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
}
执行额外的响应验证
OpenSaml5AuthenticationProvider 在解密 Response 后立即验证 Issuer 和 Destination 值。您可以通过扩展默认验证器并与您自己的响应验证器串联来自定义验证,也可以完全替换为您自己的验证器。
例如,你可以抛出一个自定义异常,其中包含 Response 对象中可用的任何附加信息,如下所示:
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 在其 BearerSubjectConfirmationValidator 类中执行 Asssertion#InResponseTo 验证,该验证可通过 setAssertionValidator 进行配置。
执行额外的断言验证
OpenSaml5AuthenticationProvider 对 SAML 2.0 断言执行最小限度的验证。在验证签名后,它将:
-
验证
<AudienceRestriction>和<DelegationRestriction>条件 -
验证
<SubjectConfirmation>,忽略任何 IP 地址信息
要进行额外的验证,你可以配置自己的断言验证器,该验证器先委托给 OpenSaml5AuthenticationProvider 的默认验证器,然后再执行自己的验证逻辑。
例如,你可以使用 OpenSAML 的 OneTimeUseConditionValidator 来验证 <OneTimeUse> 条件,如下所示:
- 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> 元素,其解密过程通过使用在 RelyingPartyRegistration 中注册的解密 Saml2X509Credential 实例来完成。
OpenSaml5AuthenticationProvider 提供了两种解密策略。响应解密器用于解密 <saml2:Response> 中的加密元素,例如 <saml2:EncryptedAssertion>。断言解密器用于解密 <saml2:Assertion> 中的加密元素,例如 <saml2:EncryptedAttribute> 和 <saml2:EncryptedID>。
你可以替换 OpenSaml5AuthenticationProvider 的默认解密策略。例如,如果你有一个单独的服务来解密 <saml2:Response> 中的断言,你可以像下面这样使用它:
- Java
- Kotlin
MyDecryptionService decryptionService = ...;
OpenSaml5AuthenticationProvider provider = new OpenSaml5AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml5AuthenticationProvider()
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 响应 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 {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = customAuthenticationManager
}
}
return http.build()
}
}
使用 Saml2AuthenticatedPrincipal
当依赖方为给定的断言方正确配置后,即可开始接收断言。一旦依赖方验证断言通过,将生成包含 Saml2AuthenticatedPrincipal 的 Saml2Authentication 对象。
这意味着你可以在控制器中这样访问主体:
- 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 会非常方便。