验证 <saml2:Response>
为了验证 SAML 2.0 响应,Spring Security 使用 Saml2AuthenticationTokenConverter
来填充 Authentication
请求,并使用 OpenSaml4AuthenticationProvider
来进行身份验证。
您可以通过多种方式配置它,包括
-
更改
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()
}
设置时钟偏差
断言方和信赖方拥有未完全同步的系统时钟并不罕见。因此,您可以使用一些容差配置 `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` 中包含用户详细信息。在这种情况下,响应身份验证转换器会派上用场,如下所示
-
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` 中提取的属性以及单个 `ROLE_USER` 权限。 |
执行额外的响应验证
OpenSaml4AuthenticationProvider
在解密 Response
后会立即验证 Issuer
和 Destination
值。您可以通过扩展默认验证器并连接您自己的响应验证器来定制验证,或者您可以完全用您自己的验证器替换它。
例如,您可以使用 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;
});
执行额外的断言验证
OpenSaml4AuthenticationProvider
对 SAML 2.0 断言执行最少的验证。在验证签名后,它将
-
验证
<AudienceRestriction>
和<DelegationRestriction>
条件 -
验证
<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) }
有两个单独的解密器,因为断言可以与响应分开签名。尝试在签名验证之前解密已签名断言的元素可能会使签名无效。如果您的断言方只对响应进行签名,那么使用响应解密器来解密所有元素是安全的。 |
使用自定义身份验证管理器
当然,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 {
authorizeRequests {
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 非常方便。
|