You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 16:52:42