Spring Cloud Gateway同一微服务兼容JWT与Double Cookie认证问询
一、CORS配置统一处理
API网关作为SPA的唯一入口,所有跨域请求由网关统一处理,身份微服务无需直接对接SPA的跨域逻辑:
- 修正网关GlobalCors配置:带Credentials的请求不允许
allowedOrigins设为*,必须指定SPA的实际域名,同时开启allowCredentials:
spring: cloud: gateway: default-filters: - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin globalcors: cors-configurations: '[/**]': allowedOrigins: "https://your-spa-domain.com" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true
- 身份微服务关闭CORS,仅允许网关访问(避免直接暴露给外部):
@Configuration @EnableWebSecurity public class AuthSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .cors().disable() // 关闭身份微服务的CORS处理 // 其他安全配置... } }
二、API网关实现双认证复合Filter
修改原有JwtAuthFilter,加入CSRF Cookie校验逻辑,优先处理JWT(原生APP),无JWT时自动切换到CSRF会话认证(SPA):
@Component public class CompositeAuthFilter extends AbstractGatewayFilterFactory<CompositeAuthFilter.Config> { private final JwtParser jwtParser; private final StringRedisTemplate redisTemplate; // 集群环境用Redis存会话,单机可直接调用身份微服务 public CompositeAuthFilter(JwtParser jwtParser, StringRedisTemplate redisTemplate) { super(Config.class); this.jwtParser = jwtParser; this.redisTemplate = redisTemplate; } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { ServerHttpRequest request = exchange.getRequest(); // 跳过OPTIONS预检请求 if (request.getMethod() == HttpMethod.OPTIONS) { return chain.filter(exchange); } // 1. 优先校验JWT令牌(原生APP) String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader != null && authHeader.startsWith("Bearer ")) { String jwtToken = authHeader.substring(7); try { jwtParser.parseClaimsJws(jwtToken); return chain.filter(exchange); } catch (JwtException e) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } // 2. 无JWT时,校验CSRF Cookie(SPA) String cookieCsrf = getCookieValue(request, "XSRF-TOKEN"); String headerCsrf = request.getHeaders().getFirst("X-XSRF-TOKEN"); String sessionId = getCookieValue(request, "SESSION"); // 校验令牌完整性 if (cookieCsrf == null || headerCsrf == null || sessionId == null) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 校验CSRF令牌一致性 if (!cookieCsrf.equals(headerCsrf)) { exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } // 校验会话有效性(Redis集群方案) if (!redisTemplate.hasKey("SESSION:" + sessionId)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }; } private String getCookieValue(ServerHttpRequest request, String cookieName) { return request.getCookies().getFirst(cookieName) != null ? request.getCookies().getFirst(cookieName).getValue() : null; } public static class Config {} }
三、路由配置简化
路由仅需绑定这个复合Filter,无需额外配置多个Filter:
spring.cloud.gateway.routes[0].id=service-1 spring.cloud.gateway.routes[0].uri=lb://service-1 spring.cloud.gateway.routes[0].predicates[0]=Path=/api/service1/* spring.cloud.gateway.routes[0].filters[0]=CompositeAuthFilter
四、身份微服务的CSRF与会话配置
调整身份微服务Security,开启CSRF并配置Cookie属性,同时实现会话存储与校验:
@Configuration @EnableWebSecurity public class AuthSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private StringRedisTemplate redisTemplate; @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 允许SPA读取XSRF-TOKEN .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 按需创建会话 .sessionIdResolver(new CustomSessionIdResolver()) // 自定义会话ID存储到Redis .and() .authorizeRequests() .antMatchers("/api/auth/login", "/api/auth/csrf-token").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/api/auth/login") .successHandler((request, response, authentication) -> { // 登录成功后,将会话ID存入Redis,设置过期时间 String sessionId = request.getSession().getId(); redisTemplate.opsForValue().set("SESSION:" + sessionId, authentication.getName(), 3600, TimeUnit.SECONDS); response.setStatus(HttpStatus.OK.value()); }); } // 自定义会话ID解析器(可选,集群环境必备) private static class CustomSessionIdResolver implements SessionIdResolver { @Override public String resolveSessionId(HttpServletRequest request) { return request.getSession().getId(); } @Override public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) { response.setHeader("X-Session-Id", sessionId); } } // 暴露CSRF令牌接口(可选,SPA也可直接从Cookie读取) @GetMapping("/api/auth/csrf-token") public ResponseEntity<String> getCsrfToken(HttpServletRequest request) { CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); return ResponseEntity.ok(csrfToken.getToken()); } }
五、关键细节
- Cookie安全属性:会话Cookie(
SESSION)需设置HttpOnly=true防XSS;XSRF-TOKEN设HttpOnly=false允许SPA读取;生产环境强制开启Secure=true(仅HTTPS传输)和SameSite=Lax。 - 集群会话共享:必须用Redis存储会话,避免身份微服务集群节点间会话不一致。
- 性能优化:网关直接从Redis校验会话,无需调用身份微服务,降低链路延迟。
内容的提问来源于stack exchange,提问作者Kinshuk Lahiri
相关产品推荐
相关产品推荐

