Spring Cloud Gateway 实战:统一鉴权与用户信息全链路透传

在单体架构中,用户登录校验通常只需编写一个拦截器(Interceptor)即可统一处理。同时,用户上下文信息(如用户 ID、角色等)可以方便地通过 ThreadLocal 在整个请求生命周期内进行存储和共享。

然而,当系统演进为微服务架构后,原有的方案面临两个关键挑战:

  1. 认证逻辑重复:每个微服务都需要独立实现登录校验逻辑,导致代码冗余、维护成本上升;
  2. 上下文传递困难:由于服务之间相互隔离,ThreadLocal无法跨服务共享用户信息,传统的线程局部变量机制不再适用。

为解决上述问题,通常引入 **API 网关(Gateway)**作为统一入口,在网关层集中完成身份认证与鉴权,并将用户上下文信息(如用户 ID、权限标识等)以标准化方式(例如通过 HTTP 请求头)透传给下游微服务。各微服务在接收到请求后,可从请求头中解析并重建本地上下文(如重新设置 ThreadLocal),从而在保持服务解耦的同时,实现安全、高效的身份传递与使用。

1. 快速入门

1. 创建项目

单独创建一个独立的模块 gateway-service用于管理网关相关内容。

2. 引入依赖

XML 复制代码
<dependencies>
    <!--网关-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!--nacos discovery-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--负载均衡-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
</dependencies>

3. 创建启动类

创建正常的项目启动类即可

java 复制代码
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

4. 配置路由

bash 复制代码
server:
  port: 8080

spring:
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: ${NACOS_SERVER_ADDR:127.0.0.1}:8848  # 建议使用环境变量提升灵活性
    gateway:
      routes:
        - id: test                      # 路由 ID,需全局唯一
          uri: lb://test-service        # 目标服务地址,`lb://` 表示启用负载均衡,从注册中心动态获取服务实例
          predicates:
            - Path=/test/**             # 路径匹配规则:所有以 `/test/` 开头的请求将被路由到 `test-service`

当请求到达网关时,Spring Cloud Gateway 会根据配置的 路由断言(Predicates) 判断是否匹配当前路由规则。若请求路径满足 /test/**,网关将:

  1. 通过服务名 text-service 从 Nacos 注册中心获取可用的服务实例列表;
  2. 基于内置的负载均衡策略(默认为 Ribbon 或 Spring Cloud LoadBalancer)选择一个具体实例;
  3. 将原始请求转发至该实例,并保留原始路径(如 /test/hello 会被完整转发);
  4. 下游微服务处理请求后,响应结果经由网关返回给客户端。

2. 网关登录校验

在微服务架构中,API 网关作为系统的统一入口,非常适合承担身份认证与权限校验的职责。通过将登录校验逻辑前置到网关层,可以实现以下优势:

  • 集中管控:所有请求必须先经过网关,避免在每个微服务中重复实现认证逻辑;
  • 安全统一:一旦校验失败,可直接在网关拦截请求,无需将无效请求转发至下游服务;
  • 上下文透传:认证成功后,可将用户关键信息(如用户 ID、角色、租户标识等)封装到 HTTP 请求头中(例如 X-User-Id、X-User-Roles),随请求一起转发给目标微服务。

下游微服务在接收到请求后,可从请求头中提取用户信息,并将其存入本地的 ThreadLocal 上下文中。这样,在当前请求处理链路中的任意位置(如 Service、DAO 层)都能便捷、高效地访问用户上下文,同时保持服务间的解耦。

1. 网关过滤器

在网管实现过滤器有两种方法:

  1. GatewayFilter
  2. GlobalFilter

2. 自定义拦截器

1. 自定义 GatewayFilter 拦截器

在 Spring Cloud Gateway 中,可以通过继承 AbstractGatewayFilterFactory实现自定义的 局部过滤器(GatewayFilter) 。该过滤器可通过 YAML 配置灵活应用于全部路由特定路由

java 复制代码
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            // 获取请求对象
            ServerHttpRequest request = exchange.getRequest();
            // 执行自定义逻辑
            System.out.println("【PrintAny 过滤器】请求路径: " + request.getURI().getPath());
            // 继续执行后续过滤器和目标服务
            return chain.filter(exchange);
        };
    }
}

命名规范说明

Spring Cloud Gateway 会根据配置中的名称(如 PrintAny)自动查找对应的 PrintAnyGatewayFilterFactoryBean。因此,类名必须以 GatewayFilterFactory 结尾,配置中只需使用前缀部分。

应用方式有两种:

1. 应用方式一:全局生效(作用于所有路由)
java 复制代码
spring:
  cloud:
    gateway:
      default-filters:
            - PrintAny
2. 应用方式二:仅作用于特定路由
java 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: test_route
          uri: lb://test-service
          predicates:
            - Path=/test/**
          filters:
            - PrintAny

这样拦截器就只会作用于一个路由下

2. 自定义 GlobalFilter 拦截器

在 Spring Cloud Gateway 中,GlobalFilter 用于实现对所有路由统一生效 的过滤逻辑(如身份认证、日志记录、限流等)。与 GatewayFilter不同,GlobalFilter无需在配置文件中声明,只需将其注册为 Spring Bean,即可在网关启动后自动生效。

java 复制代码
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求信息
        ServerHttpRequest request = exchange.getRequest();
        System.out.println("【GlobalFilter】请求路径: " + request.getURI().getPath());

        // 继续执行后续过滤器及目标服务
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 定义过滤器执行顺序:数值越小,优先级越高
        return 0;
    }
}

关键说明:

  • 自动生效 :只要类上标注了 @Component(或其他方式注册为 Spring Bean),Spring Cloud Gateway 会自动发现并将其加入全局过滤器链,无需任何 YAML 配置
  • 作用范围 :对所有进入网关的请求生效,无论其匹配哪个路由。
  • 执行顺序 :通过实现 Ordered 接口的 getOrder() 方法控制。若需在内置过滤器(如路由转发)之前执行,应返回较小的值(例如 -1 或 HIGHEST_PRECEDENCE)。

⚠️ 注意:GlobalFilter 无法像 GatewayFilter 那样"仅对部分路由生效"。如果需要条件性处理(如跳过某些路径),必须在 filter() 方法内部通过exchange.getRequest().getPath() 等方式自行判断。

3. 登录校验

在微服务架构中,用户认证应集中在 API 网关完成,而各业务微服务只需信任网关传递的用户身份信息。这既提升了安全性,又避免了重复鉴权逻辑。

整体流程如下:

  1. 网关拦截请求,校验 JWT Token
  2. 若有效,将用户 ID 注入请求头(如 user-info)并转发
  3. 下游微服务通过拦截器提取该 Header,重建本地用户上下文

1. 编写拦截代码

java 复制代码
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {

	// 构建与解析 JWT 令牌的工具
    private final JwtTool jwtTool;

    // 存放排除的路径
    private final AuthProperties authProperties;
    
    // 用来解析路径的工具
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取Request
        ServerHttpRequest request = exchange.getRequest();
		
        /**
			这里可以判断请求路径是否可以跳过
			if(isExclude(request.getPath().toString())){
				// 无需拦截,直接放行
				return chain.filter(exchange);
			}
		**/
        // 获取请求头中的token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (!CollUtils.isEmpty(headers)) {
            token = headers.get(0);
        }
        // 校验并解析token
        Long userId = null;
        try {
			// 自定义 Token 工具进行校验,与单体 Token 工具类似,可自行编写
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 如果无效,拦截
            ServerHttpResponse response = exchange.getResponse();
			// 抛出 401 状态码
            response.setRawStatusCode(401);
            return response.setComplete();
        }

        /**
			如果有效,将用户信息封装在请求头中
		**/
        String userInfo = userId.toString();
        ServerWebExchange build = exchange.mutate()
                .request(b -> b.header("user-info", userInfo))
                .build();
        // 放行
        return chain.filter(exchange);
    }

	// 判断是否是排除路径
    private boolean isExclude(String antPath) {
		/** 
			authProperties  假设这个集合中存放的是需要排除的路径
			例如:/users/login 等
			建议将这些排除路径卸载配置文件中便于后期管理
			
		**/
        for (String pathPattern : authProperties.getExcludePaths()) {
            if(antPathMatcher.match(pathPattern, antPath)){
                return true;
            }
        }
        return false;
    }

	/**
     * 设置过滤器优先级:值越小,执行越早。
     * 此处设为 0,确保在路由、重写等默认过滤器之前执行。
     */
    @Override
    public int getOrder() {
        return 0;
    }
}

2. 获取用户信息

当通过网关时会解析请请求头中 token 信息,并将解析后的用户数据存放在请求头中,传递给相应的实例,那么在每个微服务中就只需要拦截请求,然后从请求头中获取用户数据即可。

由于每个业务微服务都需要从请求头中提取用户信息,可将此逻辑封装到公共模块(如 common-starter) 中复用

java 复制代码
public class UserInfoInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取请求头中的信息
        String userInfo = request.getHeader("user-info");

        // 将信息保存在 ThreadLocal 中
        if(!StrUtil.isEmptyIfStr(userInfo)){
            UserContext.setUser(Long.valueOf(userInfo));
        }

        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求结束,清理 ThreadLocal,防止内存泄漏
        UserContext.removeUser();
    }
}

说明

  • UserContext 是基于 ThreadLocal 的工具类,用于在线程内共享用户 ID;
  • afterCompletion 确保每次请求结束后清理上下文。

在定义完拦截器之后需要注册,因为所有请求都需拦截判断请求头中是否存在用户信息,所以就是最基础的注册代码:

java 复制代码
@Configuration
// 仅当应用是基于 Servlet(即普通微服务)时才加载此配置
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

为什么加 @ConditionalOnClass(DispatcherServlet.class)?

Spring Cloud Gateway 基于 WebFlux(响应式),不包含 DispatcherServlet;

普通微服务基于 Spring MVC(Servlet 阻塞模型),包含 DispatcherServlet;

因此,该条件能自动排除网关服务,避免在非 MVC 环境中错误加载拦截器。

⚠️ 重要前提:网关项目不得引入 spring-boot-starter-web,否则会导致条件误判。

4. 微服务之间传递用户信息

在微服务架构中,当一个服务(如 order-service)通过 Feign 客户端 调用另一个服务(如 user-service)时,若需将当前登录用户的身份信息传递给被调用方,就需要在 Feign 调用链路中自动注入用户上下文。

由于所有 @FeignClient 接口通常集中定义在 公共 API 模块(如 api-service 或 xxx-api) 中,我们可以在该模块中统一注册一个 Feign 请求拦截器(RequestInterceptor),实现用户信息的自动透传。

java 复制代码
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
    return new RequestInterceptor() {
        @Override
        public void apply(RequestTemplate template) {
           // 从本地线程上下文中获取当前用户 ID
            Long userId = UserContext.getUser();
            if (userId == null) {
                // 若无用户信息(如系统内部调用),则跳过
                return;
            }
            // 将用户 ID 添加到请求头,供下游服务使用
            template.header("user-info", userId.toString());
        }
    };
}

通过以上代码,在微服务之间发起请求也会将用户信息添加在请求投中,方便被调用服务获取用户信息。

说明:

UserContext.getUser() 从 ThreadLocal 中获取当前请求绑定的用户 ID(通常由前置拦截器或网关注入);

template.header(...) 会向 Feign 发起的 HTTP 请求中添加自定义 Header;

该拦截器对所有 Feign 客户端生效,无需逐个配置。

5. 总结

以上内容的主要流程:

java 复制代码
客户端
   │
   ▼
[API 网关] 
   ├─ 1. 验证 JWT Token
   ├─ 2. 提取 userId
   ├─ 3. 从 Nacos 查找 order-service 实例
   └─ 4. 转发请求 + Header: user-info=12345
           │
           ▼
[order-service]
   ├─ 5. 拦截器解析 user-info → UserContext
   └─ 6. 通过 Feign 调用 user-service
           │
           ▼
[Feign 拦截器]
   └─ 7. 自动注入 user-info=12345 到请求头
           │
           ▼
[user-service]
   └─ 8. 拦截器重建 UserContext
相关推荐
述清-架构师之路8 小时前
【亲测可用】idea设置mvn默认版本路径,setting路径,仓库路径
java·ide·intellij-idea
往今~8 小时前
Matlab: 绘制GDS图纸
开发语言·matlab
泡泡以安10 小时前
【Android逆向工程】第3章:Java 字节码与 Smali 语法基础
android·java·安卓逆向
毕设源码-朱学姐15 小时前
【开题答辩全过程】以 工厂能耗分析平台的设计与实现为例,包含答辩的问题和答案
java·vue.js
喵了meme16 小时前
C语言实战4
c语言·开发语言
码界奇点16 小时前
Python从0到100一站式学习路线图与实战指南
开发语言·python·学习·青少年编程·贴图
9ilk16 小时前
【C++】--- 特殊类设计
开发语言·c++·后端
sali-tec16 小时前
C# 基于halcon的视觉工作流-章68 深度学习-对象检测
开发语言·算法·计算机视觉·重构·c#
Spring AI学习17 小时前
Spring AI深度解析(9/50):可观测性与监控体系实战
java·人工智能·spring