Spring Security SAML登出失败:Saml2LogoutResponseFilter验证错误及404问题
我们实现了简单的SAML2登出流程,用户在IdP完成登出后,回调应用销毁会话时出现HTTP 404错误,开启调试日志后发现如下错误:
2023-02-03 14:42:37.845 DEBUG 1 --- [p-nio-80-exec-2] .s.s.p.s.w.a.l.Saml2LogoutResponseFilter : Failed to validate LogoutResponse: [[invalid_destination] Failed to match destination to configured destination]
相关配置代码
import org.opensaml.xmlsec.signature.support.SignatureConstants; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.web.SecurityFilterChain; import java.security.PrivateKey; import java.security.cert.X509Certificate; import static org.springframework.security.config.Customizer.withDefaults; @Profile("!local") @EnableWebSecurity @Configuration public class Saml2WebSecurityConfig { private static final String LOGOUT_CALLBACK_URL = "/logout/saml2/slo"; private final EIAMConfigProperties eiamConfigProperties; public Saml2WebSecurityConfig(EIAMConfigProperties eiamConfigProperties) { this.eiamConfigProperties = eiamConfigProperties; } @Bean public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() throws Exception { final X509Certificate[] cert = ...; final PrivateKey privateKey = ...; final X509Certificate[] verificationCertificate = ...; Saml2X509Credential signingCredential = Saml2X509Credential.signing(privateKey, cert[0]); Saml2X509Credential verificationCredential = Saml2X509Credential.verification(verificationCertificate[0]); RelyingPartyRegistration registration = RelyingPartyRegistration .withRegistrationId(eiamConfigProperties.getRegistrationId()) .entityId(eiamConfigProperties.getEntity()) .signingX509Credentials(c -> c.add(signingCredential)) .assertingPartyDetails(party -> party.entityId(eiamConfigProperties.getAssertionId()) .singleSignOnServiceLocation(eiamConfigProperties.getSingleSignOnServiceLocation()) .singleSignOnServiceBinding(Saml2MessageBinding.POST) .singleLogoutServiceLocation(eiamConfigProperties.getSingleLogoutServiceLocation()) .singleLogoutServiceBinding(Saml2MessageBinding.POST) .wantAuthnRequestsSigned(true) .signingAlgorithms(sign -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256)) .verificationX509Credentials(c -> c.add(verificationCredential)) ) .build(); return new InMemoryRelyingPartyRegistrationRepository(registration); } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()) .saml2Login(withDefaults()) .saml2Logout((saml2) -> saml2 .logoutRequest((request) -> request.logoutUrl(LOGOUT_CALLBACK_URL)) .logoutResponse((response) -> response.logoutUrl(LOGOUT_CALLBACK_URL)) ); http.csrf().disable(); return http.build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().requestMatchers(LOGOUT_CALLBACK_URL); } }
问题分析
这里存在两个核心问题:
- WebSecurity忽略了登出回调URL:配置中把
/logout/saml2/slo加入了忽略列表,导致Spring Security的SAML2登出过滤器无法处理该请求,直接返回404。 - Destination地址不匹配:IdP返回的LogoutResponse中的Destination地址,与Relying Party配置的单登出回调地址不一致,触发验证失败。
修复步骤
1. 移除对登出回调URL的忽略
删除WebSecurityCustomizer中对LOGOUT_CALLBACK_URL的忽略配置,让Spring Security的SAML2过滤器可以处理该请求:
// 移除LOGOUT_CALLBACK_URL的忽略配置,保留其他需要忽略的资源(如静态文件) @Bean public WebSecurityCustomizer webSecurityCustomizer() { return web -> web.ignoring().requestMatchers("/static/**"); // 示例:仅忽略静态资源 }
2. 配置正确的SP单登出回调地址
在RelyingPartyRegistration中明确配置SP的单登出服务位置,确保和IdP后台配置的回调地址完全一致(包括协议、域名、端口和路径):
RelyingPartyRegistration registration = RelyingPartyRegistration .withRegistrationId(eiamConfigProperties.getRegistrationId()) .entityId(eiamConfigProperties.getEntity()) // 新增SP单登出回调地址,需与IdP配置完全一致 .singleLogoutServiceLocation("https://your-app-domain.com/logout/saml2/slo") .singleLogoutServiceBinding(Saml2MessageBinding.POST) .signingX509Credentials(c -> c.add(signingCredential)) .assertingPartyDetails(party -> party.entityId(eiamConfigProperties.getAssertionId()) .singleSignOnServiceLocation(eiamConfigProperties.getSingleSignOnServiceLocation()) .singleSignOnServiceBinding(Saml2MessageBinding.POST) .singleLogoutServiceLocation(eiamConfigProperties.getSingleLogoutServiceLocation()) .singleLogoutServiceBinding(Saml2MessageBinding.POST) .wantAuthnRequestsSigned(true) .signingAlgorithms(sign -> sign.add(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256)) .verificationX509Credentials(c -> c.add(verificationCredential)) ) .build();
注意:如果应用部署在反向代理后,需确保Spring Security能正确获取外部请求地址,可通过配置
server.forward-headers-strategy=NATIVE或在RelyingPartyRegistration中直接使用完整URL。
3. 简化SAML2登出配置
SecurityFilterChain中的saml2Logout无需手动指定logoutUrl,默认会使用RelyingPartyRegistration中配置的singleLogoutServiceLocation:
http.authorizeHttpRequests(authorize -> authorize .anyRequest().authenticated()) .saml2Login(withDefaults()) .saml2Logout(withDefaults());
4. 自定义会话销毁逻辑(可选)
如果需要自定义登出后的会话处理逻辑,可通过LogoutSuccessHandler实现:
.saml2Logout(saml2 -> saml2 .logoutSuccessHandler((request, response, authentication) -> { // 销毁当前会话 request.getSession().invalidate(); // 重定向到自定义登出成功页面 response.sendRedirect("/logout-success"); // 其他自定义操作,比如清除缓存、记录登出日志等 }) )
内容的提问来源于stack exchange,提问作者marco

