# 验证<saml2:Response>
s
为了验证SAML2.0响应, Spring Security默认使用[OpenSaml4AuthenticationProvider
](overview.html# Servlet-saml2login-architecture)。
你可以通过多种方式配置此功能,包括:
设置时钟倾斜以进行时间戳验证
将响应映射到
GrantedAuthority
实例的列表自定义验证断言的策略
自定义解密响应和断言元素的策略
要配置这些,你可以在DSL中使用saml2Login#authenticationManager
方法。
# 设置时钟倾斜
主张和依赖方的系统时钟AREN不能完全同步,这并不罕见。出于这个原因,你可以在一定程度上配置OpenSaml4AuthenticationProvider
的默认断言验证器:
Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(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))
);
}
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
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)
}
}
}
}
# 与UserDetailsService
协调
或者,你可能希望包含来自遗留的UserDetailsService
的用户详细信息。在这种情况下,响应身份验证转换器可以派上用场,如下所示:
Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(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))
);
}
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
var userDetailsService: UserDetailsService? = null
override fun configure(http: HttpSecurity) {
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)
}
}
}
}
1 | 首先,调用默认的转换器,它从响应中提取属性和权限。 |
---|---|
2 | 其次,使用相关信息调用[UserDetailsService ](../../身份验证/密码/user-details-service.html# Servlet-身份验证-userdetailsService) |
3 | 第三,返回包含用户详细信息的自定义身份验证 |
它不需要调用OpenSaml4AuthenticationProvider 的默认身份验证转换器。它返回一个 Saml2AuthenticatedPrincipal ,其中包含它从AttributeStatement s中提取的属性以及单个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
对SAML2.0断言执行最小的验证。在验证签名之后,它将:
验证
<AudienceRestriction>
和<DelegationRestriction>
条件验证
<SubjectConfirmation>
s,期望获得任何IP地址信息
要执行额外的验证,你可以配置自己的断言验证器,将其委托给OpenSaml4AuthenticationProvider
的默认值,然后执行自己的断言验证器。
例如,你可以使用OpenSAML的OneTimeUseConditionValidator
来验证<OneTimeUse>
条件,如下所示:
Java
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()));
});
Kotlin
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) {
[email protected] result
}
} catch (e: Exception) {
[email protected] result.concat(Saml2Error(INVALID_ASSERTION, e.message))
}
result.concat(Saml2Error(INVALID_ASSERTION, context.validationFailureMessage))
}
虽然推荐使用,但没有必要调用OpenSaml4AuthenticationProvider 的默认断言验证器。如果你不需要它来检查 <AudienceRestriction> 或<SubjectConfirmation> ,那么你就可以跳过它,因为你是在自己执行这些操作。 |
---|
# 自定义解密
Spring 安全解密<saml2:EncryptedAssertion>
,<saml2:EncryptedAttribute>
,和<saml2:EncryptedID>
元素,通过使用解密[Saml2X509Credential
实例](overview.html# Servlet-saml2login-rpr-creditions)在[RelyingPartyRegistration
]中注册的(overview.html# Servlet-saml2login-relyingpartyregistration)。
OpenSaml4AuthenticationProvider
暴露两种解密策略。响应Decrypter用于解密<saml2:Response>
中的加密元素,例如<saml2:EncryptedAssertion>
。断言解密程序用于解密<saml2:Assertion>
中的加密元素,例如<saml2:EncryptedAttribute>
和<saml2:EncryptedID>
。
可以替换OpenSaml4AuthenticationProvider’s default decryption strategy with your own. For example, if you have a separate service that decrypts the assertions in a
saml2:Response`,可以这样使用:
Java
MyDecryptionService decryptionService = ...;
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseElementsDecrypter((responseToken) -> decryptionService.decrypt(responseToken.getResponse()));
Kotlin
val decryptionService: MyDecryptionService = ...
val provider = OpenSaml4AuthenticationProvider()
provider.setResponseElementsDecrypter { responseToken -> decryptionService.decrypt(responseToken.response) }
如果你也在解密<saml2:Assertion>
中的单个元素,那么也可以自定义断言解密程序:
Java
provider.setAssertionElementsDecrypter((assertionToken) -> decryptionService.decrypt(assertionToken.getAssertion()));
Kotlin
provider.setAssertionElementsDecrypter { assertionToken -> decryptionService.decrypt(assertionToken.assertion) }
由于断言可以与响应分开签名,因此有两个单独的解密器。 在签名验证之前尝试解密签名断言的元素可能会使签名无效。 如果你的断言方只签名响应,那么只使用响应解密器解密所有元素是安全的。 |
---|
# 使用自定义身份验证管理器
当然,authenticationManager
DSL方法也可以用于执行完全自定义的SAML2.0身份验证。此身份验证管理器应该期望有一个Saml2AuthenticationToken
对象,该对象包含SAML2.0响应XML数据。
Java
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
AuthenticationManager authenticationManager = new MySaml2AuthenticationManager(...);
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.authenticationManager(authenticationManager)
)
;
}
}
Kotlin
@EnableWebSecurity
open class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
val customAuthenticationManager: AuthenticationManager = MySaml2AuthenticationManager(...)
http {
authorizeRequests {
authorize(anyRequest, authenticated)
}
saml2Login {
authenticationManager = customAuthenticationManager
}
}
}
}
# 使用Saml2AuthenticatedPrincipal
在为给定的断言方正确配置了依赖方之后,它就可以接受断言了。一旦依赖方验证了一个断言,结果就是一个Saml2Authentication
和一个Saml2AuthenticatedPrincipal
。
这意味着你可以访问控制器中的主体,如下所示:
Java
@Controller
public class MainController {
@GetMapping("/")
public String index(@AuthenticationPrincipal Saml2AuthenticatedPrincipal principal, Model model) {
String email = principal.getFirstAttribute("email");
model.setAttribute("email", email);
return "index";
}
}
Kotlin
@Controller
class MainController {
@GetMapping("/")
fun index(@AuthenticationPrincipal principal: Saml2AuthenticatedPrincipal, model: Model): String {
val email = principal.getFirstAttribute<String>("email")
model.setAttribute("email", email)
return "index"
}
}
因为SAML2.0规范允许每个属性有多个值,所以你可以调用getAttribute 来获取属性列表,也可以调用getFirstAttribute 来获取列表中的第一个。当你知道只有一个值时,getFirstAttribute 非常方便。 |
---|