Spring Cloud Gateway集成SSO时JWT令牌重定向丢失问题排查
Hey there! Let's tackle your issues one by one—starting with the architecture questions, then diving into that tricky JWT handoff problem after login.
Short answer: Don't do it (even as a temporary fix). Your current architecture follows good microservice principles:
- Separation of concerns: SSO handles user authentication and token generation; the gateway handles routing, authorization, and request filtering. Merging them would create a monolithic "god service" that's harder to maintain, scale, or update later.
- Security: Keeping the firewall locked to only the gateway port is the right call—all external traffic goes through a single, controlled entry point. Internal services (like SSO) are protected from direct external access.
- Flexibility: If you ever need to swap out the gateway (e.g., switch from Zuul to Spring Cloud Gateway) or update your SSO system (e.g., add OAuth2 support), keeping them separate makes those changes far easier.
The root issue here is how browsers handle redirects: when you add the JWT to the response header in your SSO success handler, the browser won't automatically carry that header over to the new redirect request. Browsers only persist cookies (for matching domains) across requests, not arbitrary response headers.
Here are two solid solutions, ordered by security and best practice:
Option 1: Store JWT in a Secure Cookie (Recommended)
This is the standard approach for web apps—browsers will automatically send the cookie with every request to your gateway's domain.
Update SSO's JwtAuthenticationSuccessHandler:
Replace the response header logic with a secure cookie:
@Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException, ServletException { String token = jwtTokenService.expiring(ImmutableMap.of( "email", auth.getName(), "authorities", auth.getAuthorities() .stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")))); // Create a secure, HttpOnly cookie for the JWT Cookie jwtCookie = new Cookie(jwtConfig.getHeader(), jwtConfig.getPrefix() + token); jwtCookie.setHttpOnly(true); // Prevents XSS attacks from stealing the token jwtCookie.setSecure(true); // Enable this in production (requires HTTPS) jwtCookie.setSameSite("Strict"); // Mitigates CSRF risks jwtCookie.setPath("/"); // Ensures the cookie is sent for all paths on your domain response.addCookie(jwtCookie); // Keep your existing redirect logic DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST"); String redirectUrl = defaultSavedRequest != null ? defaultSavedRequest.getRedirectUrl() : "http://localhost:7070/web"; getRedirectStrategy().sendRedirect(request, response, redirectUrl); }
Update Gateway's JwtTokenAuthenticationFilter:
Modify the filter to check for the JWT in both the header and the cookie:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String token = null; // First try to get token from Authorization header String header = request.getHeader(jwtConfig.getHeader()); if (header != null && header.startsWith(jwtConfig.getPrefix())) { token = header.replace(jwtConfig.getPrefix(), ""); } // If no header, check cookies else { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (jwtConfig.getHeader().equals(cookie.getName())) { token = cookie.getValue().replace(jwtConfig.getPrefix(), ""); break; } } } } if (token == null) { chain.doFilter(request, response); return; } // Keep your existing JWT validation and authentication logic here... }
Option 2: Append JWT as a Query Parameter (Temporary, Less Secure)
This works as a quick fix but is not recommended for production—URLs are often logged, which exposes the JWT to potential leaks.
Update SSO's Redirect Logic:
Add the JWT as a query parameter to the redirect URL:
String token = jwtTokenService.expiring(...); // Keep your token generation logic String redirectUrl = defaultSavedRequest != null ? defaultSavedRequest.getRedirectUrl() : "http://localhost:7070/web"; // Append token to the redirect URL redirectUrl += "?" + jwtConfig.getHeader() + "=" + URLEncoder.encode(jwtConfig.getPrefix() + token, StandardCharsets.UTF_8); getRedirectStrategy().sendRedirect(request, response, redirectUrl);
Update Gateway's Filter:
Add logic to extract the token from the query parameter:
// Add this to your token retrieval section else { String paramToken = request.getParameter(jwtConfig.getHeader()); if (paramToken != null && paramToken.startsWith(jwtConfig.getPrefix())) { token = paramToken.replace(jwtConfig.getPrefix(), ""); } }
- SSO Session Conflict: You set
SessionCreationPolicy.STATELESSbut userequest.getSession()to fetch the saved request—this will create a session anyway. If you want true statelessness, consider switching to OAuth2's Authorization Code Flow instead of form login. If you keep form login, you can remove the stateless session policy (since form login relies on sessions to track the original request). - Gateway Authentication Entry Point: Your current gateway config uses
formLogin().loginPage("/sso/login"), which is intended for a gateway-hosted login page. Instead, use anauthenticationEntryPointto redirect unauthenticated requests to SSO:http.exceptionHandling() .authenticationEntryPoint((req, res, ex) -> { // Save the original request so SSO can redirect back after login HttpSessionRequestCache requestCache = new HttpSessionRequestCache(); requestCache.saveRequest(req, res); res.sendRedirect("http://localhost:8080/sso/login"); }); - AuthenticatedFilter Cleanup: Your Zuul filter has placeholder code (
writeValueAsString("authenticationDto")). Make sure to replace this with the actualAuthenticationDtoobject once you've finalized it—this will pass user auth info to downstream services correctly.
Your overall architecture is sound! The idea of routing all traffic through the gateway, using SSO for centralized authentication, and Eureka for service discovery aligns with microservice best practices. Stick with separating SSO and gateway—you'll thank yourself later when you need to scale or update either component.
内容的提问来源于stack exchange,提问作者Black0Jaguar

