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
相关产品推荐
相关产品推荐

