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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 23:45:27