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

Spring Cloud API Gateway认证通过仍返回401问题求助

问题描述

基于Spring Cloud构建API Gateway,网关和user service均实现JWT认证机制。当前问题:访问需认证的接口时,网关返回401,但日志显示JWT验证已成功;将这些接口加入免认证路由后可正常访问。已将下游服务从servlet改为reactive(依赖WebFlux)以避免兼容性问题,单独调用服务时功能正常。

相关代码片段

GatewaySecurityConfig

package com.OrderIt.api_gateway.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;

@Configuration
public class GatewaySecurityConfig {

    @Autowired
    private JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)  // Updated syntax for disabling CSRF
                .authorizeExchange(exchanges -> exchanges
                                .pathMatchers(HttpMethod.POST, "/api/user").permitAll()  // Allow POST requests to /api/user
//                        .pathMatchers(HttpMethod.GET, "/api/user").permitAll()
                                .pathMatchers("/api/auth/**").permitAll()
                                .pathMatchers("/api/item/**").authenticated()// Allow unauthenticated access to /authenticate
//                        .anyExchange().authenticated()  // Require authentication for all other endpoints
                ).addFilterBefore(jwtAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)  // Disable HTTP Basic authentication
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable);
        return http.build();
    }
}

JwtAuthFilter

package com.OrderIt.api_gateway.auth;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;

import java.util.List;

@Component
public class JwtAuthFilter implements WebFilter {

    private static final Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);

    @Autowired
    private JwtUtil jwtUtil;

    private final AntPathMatcher pathMatcher = new AntPathMatcher();

    // Define the list of excluded routes with HTTP methods
    private final List<RouteConfig> excludedRoutes = List.of(
            new RouteConfig("/api/user", HttpMethod.POST), // Exclude POST requests to /api/user
            new RouteConfig("/api/auth/**", null)      // Exclude all requests to /api/auth/**
//            new RouteConfig("/api/item/**", null)
    );

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        log.debug("JwtAuthFilter is processing the request");
        String path = exchange.getRequest().getURI().getPath();
        HttpMethod method = exchange.getRequest().getMethod();

        log.debug("Incoming request path: {}, method: {}", path, method);

        // Skip JWT validation for excluded routes
        if (shouldExclude(path, method)) {
            log.debug("Skipping JWT validation for excluded route: {}, method: {}", path, method);
            return chain.filter(exchange); // Continue without validating JWT
        }

        // JWT validation starts here
        String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            log.warn("Missing or invalid Authorization header");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        String token = authHeader.substring(7); // Extract the token after "Bearer "
        if (!jwtUtil.validateToken(token)) {
            log.warn("Invalid JWT token");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        log.debug("JWT token validated successfully");

        // Forward the token to downstream services
        ServerHttpRequest request = exchange.getRequest()
                .mutate()
                .header(HttpHeaders.AUTHORIZATION, authHeader) // Forward the auth header
                .build();

        return chain.filter(exchange.mutate().request(request).build())
                .doOnSuccess(aVoid -> {
                    log.debug("Successfully completed processing for path: {}", path);
                })
                .doOnError(throwable -> {
                    log.error("Error occurred while processing for path: {}, error: {}", path, throwable.getMessage());
                })
                .then(Mono.fromRunnable(() -> {
                    log.debug("Response status code: {}", exchange.getResponse().getStatusCode());
                }));
    }

    private boolean shouldExclude(String path, HttpMethod method) {
        // Check if the path and method match any exclusion in the list
        return excludedRoutes.stream().anyMatch(routeConfig ->
                pathMatcher.match(routeConfig.getPath(), path) &&
                        (routeConfig.getMethod() == null || routeConfig.getMethod().equals(method))
        );
    }

    // Helper class to define route and method exclusions
    private static class RouteConfig {
        private final String path;
        private final HttpMethod method;

        public RouteConfig(String path, HttpMethod method) {
            this.path = path;
            this.method = method;
        }

        public String getPath() {
            return path;
        }

        public HttpMethod getMethod() {
            return method;
        }
    }
}

日志信息

ttp-nio-3] [                                                 ] c.O.api_gateway.auth.JwtAuthFilter       : JwtAuthFilter is processing the request
2024-10-29T01:21:22.102+05:30 DEBUG 32596 --- [api-gateway] [ctor-http-nio-3] [                                                 ] c.O.api_gateway.auth.JwtAuthFilter       : Incoming request path: /api/item, method: POST
2024-10-29T01:21:22.658+05:30 DEBUG 32596 --- [api-gateway] [ctor-http-nio-3] [                                                 ] c.O.api_gateway.auth.JwtAuthFilter       : JWT token validated successfully
2024-10-29T01:21:22.675+05:30 DEBUG 32596 --- [api-gateway] [     parallel-4] [                                                 ] c.O.api_gateway.auth.JwtAuthFilter       : Successfully completed processing for path: /api/item
2024-10-29T01:21:22.675+05:30 DEBUG 32596 --- [api-gateway] [     parallel-4] [                                                 ] c.O.api_gateway.auth.JwtAuthFilter       : Response status code: 401 UNAUTHORIZED

原因分析

  • Spring Security上下文未更新:JwtAuthFilter仅完成JWT验证,但未将认证信息写入Spring Security的SecurityContext。网关的authorizeExchange规则要求/api/item/**必须认证,由于SecurityContext中无用户认证信息,Spring Security仍判定为未认证并返回401。
  • 过滤器逻辑缺失:自定义过滤器仅验证了JWT有效性,但未完成Spring Security认证流程的关键步骤——设置认证上下文,导致后续安全拦截器无法识别已通过验证的用户。

排查建议

1. 在JwtAuthFilter中补充SecurityContext设置

验证JWT成功后,构建Authentication对象并写入SecurityContext:

// 注入ServerSecurityContextRepository
@Autowired
private ServerSecurityContextRepository securityContextRepository;

// JWT验证成功后添加以下代码
String username = jwtUtil.extractUsername(token);
Authentication authentication = new UsernamePasswordAuthenticationToken(
        username,
        null,
        Collections.emptyList() // 若有权限信息,替换为实际权限列表
);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);

// 保存上下文并继续执行过滤器链
return securityContextRepository.save(exchange, securityContext)
        .then(chain.filter(exchange.mutate().request(request).build()));

2. 调整SecurityWebFilterChain配置

  • 暂时将.pathMatchers("/api/item/**").authenticated()替换为anyExchange().authenticated(),测试是否仍出现401,排查规则配置是否存在冲突。
  • 确认过滤器顺序:addFilterBefore(jwtAuthFilter, SecurityWebFiltersOrder.AUTHENTICATION)是否确保JWT验证在Spring Security认证拦截器之前执行。

3. 验证下游服务的认证转发

在下游服务中添加日志,确认Authorization头是否被网关正确转发。若下游服务仍在执行JWT验证,需确保网关转发的Token与服务端验证逻辑兼容。

4. 开启Spring Security调试日志

添加日志配置,查看认证流程细节,确认SecurityContext是否被正确设置:

logging.level.org.springframework.security=DEBUG

内容的提问来源于stack exchange,提问作者Arunraj V R

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 16:50:59