Spring Boot SAML SSO集成场景下自定义JWT令牌过期后的刷新机制问询
解决JWT过期后通过SAML SSO会话刷新令牌的方案
首先明确:不需要直接调用/saml/login端点,直接调用会触发完整的重定向流程,用户体验不好(即使IDP会话存在,也可能有不必要的页面跳转)。我们可以利用SAML的静默认证机制,在不打扰用户的前提下验证现有SSO会话有效性,进而生成新的JWT。
下面是具体的实现思路和步骤:
核心逻辑:依赖IDP的活跃会话进行静默认证
因为你的应用是无状态的,后端不存储用户会话,所以刷新JWT的关键是验证用户在IDP(比如Azure AD)的会话是否仍处于活跃状态。只要IDP会话有效,就能静默获取新的SAML断言,进而生成新的JWT。
具体实现步骤
1. 前端监听JWT过期,发起静默刷新请求
在Angular前端,你可以:
- 用拦截器监听所有API请求,检查JWT的过期时间(比如提前5分钟触发刷新)
- 当令牌快过期时,发起一个静默请求到后端的刷新接口(比如
/api/saml/refresh-jwt) - 这个请求可以用iframe发起,避免页面跳转,实现无感知刷新
示例Angular代码片段(伪代码):
// JWT拦截器中检测过期逻辑 const isTokenExpiringSoon = (token: string): boolean => { const payload = JSON.parse(atob(token.split('.')[1])); const expTime = payload.exp * 1000; return expTime - Date.now() < 5 * 60 * 1000; // 提前5分钟刷新 }; // 发起静默刷新 if (isTokenExpiringSoon(currentToken)) { const refreshIframe = document.createElement('iframe'); refreshIframe.src = '/api/saml/refresh-jwt?refresh=true'; refreshIframe.style.display = 'none'; document.body.appendChild(refreshIframe); // 监听iframe加载完成后移除 refreshIframe.onload = () => { document.body.removeChild(refreshIframe); }; }
2. 后端新增静默刷新接口,触发SAML静默认证
在Spring Boot后端,新增一个接口,利用Spring Security SAML的SAMLEntryPoint构造不带强制认证的AuthnRequest:
- 设置
ForceAuthn=false,告诉IDP如果用户已有活跃会话,直接返回断言,不需要重新登录 - 让这个请求走你现有的SAML认证流程,最终进入
onSSOSuccessHandler处理
示例后端代码片段:
@GetMapping("/api/saml/refresh-jwt") public void refreshJwt(HttpServletRequest request, HttpServletResponse response) throws Exception { SAMLEntryPoint samlEntryPoint = applicationContext.getBean(SAMLEntryPoint.class); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 构造AuthnRequest,禁用强制认证,允许IDP复用现有会话 AuthnRequest authnRequest = samlEntryPoint.getAuthnRequest(request, response, authentication); authnRequest.setForceAuthn(Boolean.FALSE); // 重定向到IDP进行静默认证 samlEntryPoint.commence(request, response, authentication); }
3. 修改onSSOSuccessHandler,区分正常登录和刷新请求
在你现有的onSSOSuccessHandler中,添加逻辑判断请求来源:
- 如果是刷新请求(通过请求参数标记),直接生成新的JWT并设置到Cookie中,返回200状态码
- 如果是正常登录请求,继续执行原有的跳转逻辑
示例代码片段:
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 生成自定义JWT令牌 String jwtToken = generateJwtToken(authentication); // 判断是否为刷新请求 boolean isRefreshRequest = "true".equals(request.getParameter("refresh")); if (isRefreshRequest) { // 设置JWT到HttpOnly Cookie,有效期1小时 Cookie jwtCookie = new Cookie("JWT_TOKEN", jwtToken); jwtCookie.setHttpOnly(true); jwtCookie.setSecure(true); // 生产环境开启 jwtCookie.setMaxAge(3600); jwtCookie.setPath("/"); response.addCookie(jwtCookie); // 返回成功状态,不跳转页面 response.setStatus(HttpServletResponse.SC_OK); } else { // 正常登录流程,跳转到前端首页 response.sendRedirect("/frontend/home"); } }
关键注意事项
- IDP会话有效期:确保IDP的会话有效期长于JWT的有效期(比如设置为24小时),否则SAML会话过期后,必须重新登录
- iframe限制:部分IDP(比如Azure AD)默认不允许在iframe中处理认证,需要在IDP的应用配置中添加你的前端域名到允许的frame列表
- 无状态保障:后端不要存储任何会话信息,所有认证状态都依赖IDP的断言和JWT,保持应用的无状态特性
- 安全防护:刷新接口要添加CSRF防护,避免跨站请求伪造攻击
内容的提问来源于stack exchange,提问作者MobDev




