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

Spring Security中SAML令牌过期前重定向至用户原页面的实现问题

解决Spring Security SAML SSO中令牌过期后重定向回原页面的问题

我之前也碰到过一模一样的情况——用HttpSessionRequestCache在SAML登录成功处理器里拿不到缓存请求,核心原因是SAML的认证流程和普通表单登录的请求缓存逻辑完全不一样:普通表单登录是Spring Security拦截未授权请求后直接缓存,再跳转到登录页;但SAML流程里,SP会先重定向到IDP,这个过程中原请求的缓存要么没被正确触发,要么被SAML的过滤器链跳过了。

下面是我亲测有效的几个解决方案:


方案一:调整RequestCache配置,确保在SAML过滤器前生效

首先要让请求缓存逻辑在SAML的认证过滤器之前就拦截并保存未授权请求,而不是等SAML流程走完才处理。可以这样配置:

@Bean
public RequestCache requestCache() {
    HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
    // 只缓存GET请求,避免POST等请求重复提交的问题
    requestCache.setRequestMatcher(new AntPathRequestMatcher("/**", "GET"));
    return requestCache;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .requestCache()
            .requestCache(requestCache())
            .and()
        // 其他SAML核心配置
        .apply(saml())
            .userDetailsService(userDetailsService())
            .samlAuthenticationSuccessHandler(customSamlSuccessHandler())
            // 补充你的SAML元数据、IDP配置等
            ;
}

这样配置后,未授权的GET请求会在进入SAML流程前被缓存,登录成功后就能通过requestCache.getRequest(request, response)拿到原请求了。


方案二:手动保存原URL到Session,登录成功后取出

如果上面的方案还是拿不到缓存,那就换个思路:在用户访问受保护资源时,手动把目标URL存到Session里,登录成功后直接取这个值重定向。

步骤1:添加拦截器保存原URL

写一个拦截器,只在用户未认证时保存当前请求的完整URL:

public class SavedUrlInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        // 仅对未认证用户保存URL
        if (auth == null || !auth.isAuthenticated()) {
            StringBuilder targetUrl = new StringBuilder(request.getRequestURL());
            String queryString = request.getQueryString();
            if (queryString != null) {
                targetUrl.append("?").append(queryString);
            }
            request.getSession().setAttribute("SAVED_TARGET_URL", targetUrl.toString());
        }
        return true;
    }
}

然后在Spring MVC里注册这个拦截器,指定拦截受保护的路径:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new SavedUrlInterceptor())
            .addPathPatterns("/dashboard/**", "/profile/**"); // 替换成你的受保护路径
}

步骤2:自定义登录成功处理器取出URL

public class CustomSamlSuccessHandler extends SimpleUrlSamlAuthenticationSuccessHandler {

    @Override
    protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        // 从Session取出保存的URL
        String savedUrl = (String) request.getSession().getAttribute("SAVED_TARGET_URL");
        if (savedUrl != null) {
            // 取出后删除,避免重复使用
            request.getSession().removeAttribute("SAVED_TARGET_URL");
            return savedUrl;
        }
        // 没有保存的URL时, fallback到默认首页
        return super.determineTargetUrl(request, response, authentication);
    }
}

最后在SAML配置里指定这个成功处理器:

@Bean
public SAMLAuthenticationSuccessHandler customSamlSuccessHandler() {
    CustomSamlSuccessHandler handler = new CustomSamlSuccessHandler();
    handler.setDefaultTargetUrl("/");
    return handler;
}

方案三:利用SAML的RelayState参数传递原URL

SAML协议本身提供了RelayState参数,用来在SP和IDP之间传递状态信息。我们可以把原请求URL作为RelayState传给IDP,认证完成后再从回调里取出来。

步骤1:自定义请求Resolver设置RelayState

public class CustomAuthRequestResolver extends DefaultSAMLAuthenticationRequestResolver {

    @Override
    protected String getRelayState(SAMLMessageContext context) {
        HttpServletRequest request = context.getHttpServletRequest();
        // 组装当前请求的完整URL作为RelayState
        StringBuilder targetUrl = new StringBuilder(request.getRequestURL());
        String queryString = request.getQueryString();
        if (queryString != null) {
            targetUrl.append("?").append(queryString);
        }
        return targetUrl.toString();
    }
}

在SAML配置里替换默认的Resolver:

@Bean
public SAMLAuthenticationRequestResolver samlAuthRequestResolver() {
    CustomAuthRequestResolver resolver = new CustomAuthRequestResolver();
    resolver.setSamlContextProvider(samlContextProvider());
    return resolver;
}

// 应用到SAML配置中
.apply(saml())
    .authenticationRequestResolver(samlAuthRequestResolver())
    // 其他SAML配置

步骤2:在成功处理器里取出RelayState

@Override
protected String determineTargetUrl(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    if (authentication instanceof SAMLAuthenticationToken) {
        SAMLAuthenticationToken samlToken = (SAMLAuthenticationToken) authentication;
        String relayState = samlToken.getRelayState();
        if (relayState != null && !relayState.isEmpty()) {
            return relayState;
        }
    }
    return super.determineTargetUrl(request, response, authentication);
}

几个注意点

  • 确保Session在SAML流程中是连续的:如果IDP和SP跨域,要配置Cookie的Domain属性,或者用Redis等做Session共享。
  • 不要缓存POST请求:避免用户登录成功后重复提交表单,只针对GET请求做缓存或保存操作。
  • 测试时要模拟真实场景:先访问受保护页面触发认证,而不是直接访问SAML登录入口,这样原请求才会被捕获。

内容的提问来源于stack exchange,提问作者mukesh36

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.27 17:52:35