Spring 6迁移后多并发请求下SAML2 InResponseTo验证失败求助
Spring Security 6.2.x SAML2多窗口并发SSO 401问题解决方案
问题背景
从Spring 5.6.x迁移至6.2.x版本后,使用SAML2实现SSO认证的系统在生产环境出现部分会话401错误,日志提示Failed to match SubjectConfirmationData@InResponseTo of: #RequestId#,该问题仅在用户多浏览器窗口并发操作时触发。
复现与根源
复现步骤:打开多个窗口,设置HTTP会话超时1分钟,超时后点击确认,发送6个并行SSO请求。
问题核心在于默认的HttpSessionSaml2AuthenticationRequestRepository使用静态键HttpSessionSaml2AuthenticationRequestRepository.SAML@_AUTHN_REQUEST存储认证请求,导致同一会话内的Saml2PostAuthenticationRequest被后续请求覆盖:
- SSO请求1(ID:arqxdedtww):会话中存入该请求并发送至IDP
- SSO请求2(ID:wdwhwehwehw,同会话):覆盖会话中的请求记录
- 请求1的响应返回时,会话中已无对应ID,验证失败;请求2的响应返回时上下文已被清除,同样抛出异常
注:Spring 5.6.x无此问题,InResponseTo验证是5.7.x新增的安全校验特性。
解决方案
方案1:禁用InResponseTo验证
通过自定义Saml2AuthenticationTokenConverter跳过InResponseTo匹配逻辑,适用于无法修改存储逻辑的临时场景(注意:此方案会降低SSO安全性):
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.authentication.Saml2ErrorCodes; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationTokenConverter; public class NoInResponseToValidationConverter implements Saml2AuthenticationTokenConverter { private final Saml2AuthenticationTokenConverter delegate = new OpenSaml4AuthenticationTokenConverter(); @Override public Saml2AuthenticationToken convert(Saml2AuthenticationTokenConverterParameters parameters) { Saml2AuthenticationToken token = this.delegate.convert(parameters); if (token != null && token.getError() != null) { if (Saml2ErrorCodes.IN_RESPONSE_TO_MISMATCH.equals(token.getError().getErrorCode())) { return new Saml2AuthenticationToken( token.getPrincipal(), token.getCredentials(), token.getAuthorities(), token.getSaml2Response(), token.getRelayState() ); } } return token; } }
在Security配置中替换默认转换器:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationTokenConverter; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; @Configuration public class Saml2SecurityConfig { @Bean public Saml2AuthenticationTokenConverter saml2AuthenticationTokenConverter() { return new NoInResponseToValidationConverter(); } @Bean public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver() { OpenSaml4AuthenticationRequestResolver resolver = new OpenSaml4AuthenticationRequestResolver(); resolver.setAuthenticationTokenConverter(saml2AuthenticationTokenConverter()); return resolver; } }
方案2:自定义AuthenticationRequestRepository,使用Hazelcast存储请求
通过实现Saml2AuthenticationRequestRepository,用Hazelcast作为分布式存储载体,以唯一relayState为键存储认证请求,避免同会话覆盖问题,适合多节点集群与多窗口并发场景:
1. 实现自定义Repository
import com.hazelcast.core.HazelcastInstance; import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.UUID; import java.util.concurrent.TimeUnit; public class HazelcastSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> { private static final long EXPIRATION_MINUTES = 10; private final HazelcastInstance hazelcastInstance; public HazelcastSaml2AuthenticationRequestRepository(HazelcastInstance hazelcastInstance) { this.hazelcastInstance = hazelcastInstance; } @Override public Saml2PostAuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) { String relayState = request.getParameter("RelayState"); if (relayState == null) { return null; } return this.hazelcastInstance.getMap("saml-authn-requests").remove(relayState); } @Override public void saveAuthenticationRequest(Saml2PostAuthenticationRequest authnRequest, HttpServletRequest request, HttpServletResponse response) { String relayState = UUID.randomUUID().toString(); this.hazelcastInstance.getMap("saml-authn-requests") .put(relayState, authnRequest, EXPIRATION_MINUTES, TimeUnit.MINUTES); request.setAttribute("RelayState", relayState); } @Override public Saml2PostAuthenticationRequest removeAuthenticationRequest(HttpServletRequest request, HttpServletResponse response) { return loadAuthenticationRequest(request); } }
2. 配置Security使用自定义Repository
import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestRepository; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationRequestFilter; @Configuration public class Saml2SecurityConfig { @Bean public HazelcastInstance hazelcastInstance() { return Hazelcast.newHazelcastInstance(); } @Bean public Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> saml2AuthenticationRequestRepository(HazelcastInstance hazelcastInstance) { return new HazelcastSaml2AuthenticationRequestRepository(hazelcastInstance); } @Bean public Saml2WebSsoAuthenticationRequestFilter saml2WebSsoAuthenticationRequestFilter( Saml2AuthenticationRequestRepository<Saml2PostAuthenticationRequest> repository) { Saml2WebSsoAuthenticationRequestFilter filter = new Saml2WebSsoAuthenticationRequestFilter(); filter.setAuthenticationRequestRepository(repository); return filter; } }
内容的提问来源于stack exchange,提问作者Sid
相关产品推荐
相关产品推荐

