Gateway 网关 快速开始

发布于:2025-04-08 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、核心概念

  • 路由(route)

路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组断言工厂、一组Filter组成。如果断言为真,则说明请求的 URL 和配置的路由匹配。

  • 断言(predicates)

断言函数允许开发者去定义匹配 Http Request 中的任何信息,比如请求头和参数等。

  • 过滤器(Filter)

SpringCloud Gateway 中的 filter 分为 Gateway FilIer和 Global Filter。Filter 可以对请求和响应进行处理。

二、快速开始

假设我们现在有一个父工程 mall4cloud,它有一个子模块 user,现在我们要新建一个网关项目,以便对 user 的路由转发以及后续的认证授权。

 

user 对外提供服务:

// 请求路径为 http://localhost:8020/user/findUserById/1
@RestController
@RequestMapping("/user")
public class UserController {

    @Value("${server.port}")
    private String port;

    @RequestMapping("/findUserById/{id}")
    public String findUserById(@PathVariable String id) {
        // 模拟从数据库获取用户
        Map<String, String> map = new HashMap<>();
        map.put("1", "小林:"+port);
        map.put("2", "小王:"+port);
        return map.get(id);
    }

}
-----------------application.yml-----------------
server:
  port: 8020

spring:
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
  application:
    name: user-service

三、项目搭建

3.1、新建一个子模块 gateway

3.2、引入依赖

<!--Spring Cloud Gateway 网关场景启动器 -->
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

3.3、修改 application.yml

server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes: # 路由数组[路由-就是指定当请求满足什么条件的时候请求转发到哪个微服务]
        - id: user_route # 当前路由的标识, 要求唯一
          uri: http://localhost:8020 # 请求要转发到的地址
          order: 1 # 路由的优先级,数字越小级别越高
          predicates: # 断言[就是路由转发要满足的条件]
            - Path=/user-service/** # 当请求路径满足 Path 指定的规则时,才进行路由转发
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径

 配置参数,都在 RouteDefinition 类里面:

@Validated
public class RouteDefinition {
    private String id;
    private @NotEmpty @Valid List<PredicateDefinition> predicates = new ArrayList();
    private @Valid List<FilterDefinition> filters = new ArrayList();
    private @NotNull URI uri;
    private Map<String, Object> metadata = new HashMap();
    private int order = 0;
    //......
}

3.4、访问测试

启动 user、gateway,浏览器访问:http://localhost:8050/user-service/user/findUserById/1

请求先到达 gateway 8050,gateway 根据断言 predicates 判断当前请求路径是否满足 Path 指定的规则(匹配以 /user-service/ 开头的任意路径,/user-service/a, /user-service/a/b 都满足要求,但 http://localhost:8050/a/user-service/b 不满足要求),当前请求满足断言规则,请求需要转发到 http://localhost:8020/user-service/user/findUserById/1,在请求转发之前经过 StripPrefixGatewayFilterFactory 过滤器对请求路径进行加工,去掉1层路径,请求变为http://localhost:8020/user/findUserById/1 访问到 UserController#findUserById。

修改一下路径:http://localhost:8050/a/user-service/user/findUserById/1

当前配置存在一个问题:

当我的 user 服务有多个的时候,比如 8020、8021 当前配置无法进行负载均衡的访问。

四、接入 Nacos

4.1、引入 Nacos 依赖

<dependencies>
    <!--Spring Cloud Gateway 网关场景启动器 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <!--Spring Cloud Alibaba Nacos 注册中心客户端 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <!-- 新版本的 Nacos 不再依赖 Ribbon -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>
</dependencies>

4.2、修改 application.yml

server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery: # nacos 注册中心地址
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      routes: # 路由数组[路由-就是指定当请求满足什么条件的时候请求转发到哪个微服务]
        - id: user_route # 当前路由的标识, 要求唯一
          uri: lb://user-service # 请求要转发到的地址
          order: 1 # 路由的优先级,数字越小级别越高
          predicates: # 断言[就是路由转发要满足的条件]
            - Path=/user-service/** # 当请求路径满足 Path 指定的规则时,才进行路由转发
          filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
            - StripPrefix=1 # 转发之前去掉1层路径

uri 改为 lb://user-service

user-service 是 nacos 注册的服务名。

lb 表示负载均衡(loadbalance)地访问 user-service。

 4.3、启动服务

启动两个 user 服务:user-8020、user-8021

可以通过在 8020 右键-Copy Configuration 配置 VM Options 来启动多个服务实例。

4.4、访问测试

多次访问 http://localhost:8050/user-service/user/findUserById/1

会发现,用户服务在 8020、8021 之间来回切换。

 

五、自动寻找服务

如果请求的路径,是严格按照 网关地址/微服务名/接口的格式,那 yml 配置可以简化为:

server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      discovery:
        locator:
          enabled: true
访问的结果没有任何差别。

六、路由断言工厂(Route Predicate Factories

  • 作用

路由断言工厂根据请求的各种属性(如路径、请求方法、请求头、请求参数等)来判断请求是否符合预设的条件。如果请求满足断言工厂定义的条件,那么该请求就会被路由到对应的目标URI。如果不满足,则返回 404。

6.1、基于Datetime类型的断言工厂

6.1.1、AfterRoutePredicateFactory

接收一个日期参数(ZonedDateTime),判断请求日期是否晚于指定日期。
// 假设我现在时间是:
ZonedDateTime dateTime = ZonedDateTime.now();
System.out.println(dateTime);

2025-04-05T15:36:10.158+08:00[Asia/Shanghai]
server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: after_route
          uri: lb://user-service
          predicates:
            - After=2025-04-05T14:36:10.158+08:00[Asia/Shanghai]
          filters:
            - StripPrefix=1

访问 http://localhost:8050/user-service/user/findUserById/1

  • 修改时间
After=2025-04-05T16:36:10.158+08:00[Asia/Shanghai]

当前时间 没有 After 配置的时间,访问 404。

6.1.2、BeforeRoutePredicateFactory

接收一个日期参数(ZonedDateTime),判断请求日期是否早于指定日期。

Before=2025-04-05T16:36:10.158+08:00[Asia/Shanghai]

6.1.3、BetweenRoutePredicateFactory

接收两个日期参数(ZonedDateTime),判断请求日期是否在指定时间段内。
Between=2025-04-05T14:36:10.158+08:00[Asia/Shanghai],2025-04-05T16:36:10.158+08:00[Asia/Shanghai]

6.2、CookieRoutePredicateFactory

接收两个参数,cookie 字段名和一个正则表达式。判断 cookie 中是否具有给定的字段名且字段值与正则表达式匹配。
# 判断 Cookie 中是否有 token 这个参数
# 且 token 的值要符合 UUID 格式
Cookie=token,^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$

直接请求 http://localhost:8050/user-service/user/findUserById/1

加上 Cookie:

6.3、HeaderRoutePredicateFactory

接收两个参数,Header 字段名和正则表达式。判断请求头中是否具有给定字段名且字段值与正则表达式匹配。

# \d+ 表示数字的正则
Header=X-Request-Id,\d+

6.4、HostRoutePredicateFactory

接收一个参数,主机名模式列表。判断请求的 Host 是否满足匹配规则。

Host=**.somehost.org,**.anotherhost.org

 www.somehost.org 或者 beta.somehost.org 或者 www.anotherhost.org 域名的请求都能匹配。

6.5、MethodRoutePredicateFactory

接收一个参数,判断请求类型是否跟指定的类型匹配。

Method=POST

GET 请求: 

POST 请求:

6.6、PathRoutePredicateFactory

接收一个参数,判断请求的URI部分是否满足路径规则列表中的一个。

# {segment} 占位符
Path=/user-service/user/findUserById/{segment}

6.7、QueryRoutePredicateFactory

接收两个参数,一个必填的请求参数和一个可选的正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。

# . 表示任意一个字符
- Query=green,gree.

 请求 http://localhost:8050/user-service/user/findUserById/1?green=greet

 6.8、RemoteAddrRoutePredicateFactory

接收一个IP地址段,判断请求主机地址是否在地址段中。

# 假设我本机IP是 192.168.101.2
# /24 表示子网掩码的长度为 24 位
# 范围为 192.168.101.1 到 192.168.101.254
# 去掉网络地址 192.168.101.0 
# 去掉广播地址 192.168.101.255
RemoteAddr=192.168.101.1/24
在局域网上的另一台电脑访问  http://192.168.101.2:8050/user-service/user/findUserById/1

 

6.9、WeightRoutePredicateFactory

接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发

server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: user-high
          uri: http://localhost:8020
          predicates:
            - Weight=user,8
          filters:
            - StripPrefix=1
        - id: user-low
          uri: http://localhost:8021
          predicates:
            - Weight=user,2
          filters:
            - StripPrefix=1

权重是相对权重。像上面 user 群里,只有 8020、8021 两个服务。它们权重比值 8:2。

则 80%的请求会被路由到 8020。

七、自定义路由断言工厂

假设我们现在有一个断言,它会根据请求头里面有没有我们配置的用户权限,来决定路由转发。

Authorization=user.add,user.get,user.delete,user.update
POST http://localhost:8050/user-service/user/findUserById/1
Authorization: user.get

 我们看看在这个场景下,用自定义路由断言工厂怎么来实现。

7.1、步骤

自定义路由断言工厂步骤如下:

  1. 必须是 Spring 组件 Bean。
  2. 类必须加上 RoutePredicateFactory 作为结尾。
  3. 必须继承 AbstractRoutePredicateFactory。
  4. 必须声明静态内部类,声明属性来接收配置文件中对应的断言的信息。
  5. 需要结合 shortcutFieldOrder 进行绑定。
  6. 重写 apply 方法。在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息进行逻辑判断。true 表示匹配成功,进行路由转发;false 表示匹配失败,返回 404。

可以参考 Spring Gateway 已有的路由断言工厂 HostRoutePredicateFactory、HeaderRoutePredicateFactory、PathRoutePredicateFactory 等来写。

// 1、必须是 Spring 组件 Bean:@Component
// 2、类必须加上 RoutePredicateFactory 作为结尾:AuthorizationRoutePredicateFactory
// 3、必须继承 AbstractRoutePredicateFactory
@Component
public class AuthorizationRoutePredicateFactory extends AbstractRoutePredicateFactory<AuthorizationRoutePredicateFactory.Config> {

    public AuthorizationRoutePredicateFactory() {
        super(Config.class);
    }
    // 5、如果配置信息是逗号隔开的数组,这个方法不能少
    public ShortcutConfigurable.ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }
    // 5、需要结合 shortcutFieldOrder 进行绑定
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("auths");
    }

    // 6、重写 apply 方法
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        final boolean hasList = !ObjectUtils.isEmpty(config.auths);
        return new GatewayPredicate() {

            @Override
            public boolean test(ServerWebExchange exchange) {
                // 拿到请求头配置的 Authorization 字段值
                List<String> values = exchange.getRequest().getHeaders().getOrDefault("Authorization", Collections.emptyList());
                if (values.isEmpty()) {
                    return false;
                } else if (hasList) {
                    for (String value : values) {
                        // 判单请求中是否包含配置的权限
                        if (config.auths.contains(value)) {
                            return true;
                        }
                    }
                    return false;
                } else {
                    return true;
                }
            }

            public String toString() {
                return String.format("Authorization= %s", config.auths);
            }
        };
    }

    // 4、必须声明静态内部类
    @Validated
    public static class Config {
        // 4、声明属性来接收配置文件中对应的断言的信息
        // 接收 Authorization= 后面的 user.add,user.get,user.delete,user.update 信息
        private @NotEmpty List<String> auths = new ArrayList<>();

        public Config() {
        }

        public @NotEmpty List<String> getAuths() {
            return auths;
        }

        public Config setAuths(List<String> auths) {
            this.auths = auths;
            return this;
        }

        public Config setAuths(String... auths) {
            this.auths = Arrays.asList(auths);
            return this;
        }

    }
}

 

八、过滤器工厂(GatewayFilter Factories

路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。路由过滤器作用于特定的路由。Spring Cloud Gateway 包含许多内置的过滤器工厂。

8.1、AddRequestHeaderGatewayFilterFactory

为原始请求添加Header。接收两个参数,Header 字段名和字段值。

spring:
  cloud:
    gateway:
      routes:
        - id: user_route
          uri: lb://user-service
          predicates:
            - Path=/user-service/**
          filters:
            - StripPrefix=1
            - AddRequestHeader=Authorization,feign123

 请求到达 Gateway,在请求头增加 Authorization=feign123,在 user 校验这个 token

@RestController
@RequestMapping("/user")
public class UserController {

    @Value("${server.port}")
    private String port;

    @RequestMapping("/findUserNeedAuth/{id}")
    public String findUserNeedAuth(@PathVariable String id, @RequestHeader("Authorization") String token) {
        if (StringUtils.isBlank(token)) {
            throw new IllegalArgumentException("参数缺失!");
        }
        // 校验token
        if (!"feign123".equals(token)) {
            throw new  IllegalArgumentException("token错误!");
        }
        // 模拟从数据库获取用户
        Map<String, String> map = new HashMap<>();
        map.put("1", "小林:"+port);
        map.put("2", "小王:"+port);
        return map.get(id);
    }
}

8.2、AddRequestParameterGatewayFilterFactory

为原始请求添加请求参数。接收两个参数,参数名称和值。

spring:
  cloud:
    gateway:
      routes:
        - id: user_route
          uri: lb://user-service
          predicates:
            - Path=/user-service/**
          filters:
            - StripPrefix=1
            - AddRequestParameter=source,gateway
@RestController
@RequestMapping("/user")
public class UserController {

    @Value("${server.port}")
    private String port;

    @RequestMapping("/echo")
    public String echo(@RequestParam("source") String source) {
        return port + " 收到 " + source + " 转发过来的请求";
    }
}

 8.3、PrefixPathGatewayFilterFactory

为原始请求路径添加前缀。接收一个参数,前缀路径。

假设我们现在 user 模块配置了请求上下文路径。

server:
  port: 8020
  servlet:
    context-path: /mall4user

请求路径为 http://localhost:8020/mall4user/user/findUserById/1

 

网关想继续能转发请求成功,则需要:

spring:
  cloud:
    gateway:
      routes:
        - id: user_route
          uri: lb://user-service
          predicates:
            - Path=/user-service/**
          filters:
            - StripPrefix=1
            - PrefixPath=/mall4user

 http://localhost:8050/user-service/user/findUserById/1

 

http://localhost:8050/user-service/user/findUserById/1

请求到达网关,先经过 StripPrefix 过滤器,变为:

http://localhost:8050/user/findUserById/1

再经过 PrefixPath 过滤器,变为:

http://localhost:8050/mall4user/user/findUserById/1

最后经过 ReactiveLoadBalancerClientFilter 全局过滤器,根据负载均衡策略,

将请求转发到

http://localhost:8020/mall4user/user/findUserById/1 或

http://localhost:8021/mall4user/user/findUserById/1

九、自定义过滤器工厂

假设现在我们需要统计某一个微服务的访问量,并将统计数据存放到 Redis。

我们看看在这个场景下,用自定义过滤器工厂怎么来实现。

9.1、引入 Redis 依赖

  • pom.xml
<!-- redis 场景启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  •  application.yml
spring:
  redis:
    host: localhost
    port: 6379
  •  RedisConfig
@Configuration
public class RedisConfig {

    /**
     * 配置 Redis 连接工厂
     * @return LettuceConnectionFactory 实例
     */
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        // 使用 Lettuce 作为 Redis 客户端创建连接工厂
        return new LettuceConnectionFactory();
    }

    /**
     * 配置 RedisTemplate
     * @param redisConnectionFactory Redis 连接工厂
     * @return RedisTemplate 实例
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接工厂
        template.setConnectionFactory(redisConnectionFactory);
        // 设置键的序列化器为 StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        // 设置值的序列化器为 GenericJackson2JsonRedisSerializer,支持 JSON 序列化
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 设置哈希键的序列化器为 StringRedisSerializer
        template.setHashKeySerializer(new StringRedisSerializer());
        // 设置哈希值的序列化器为 GenericJackson2JsonRedisSerializer
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 初始化模板
        template.afterPropertiesSet();
        return template;
    }
}

9.2、步骤

跟自定义路由断言工厂类似,自定义过滤器工厂步骤如下:

  1. 必须是 Spring 组件 Bean。
  2. 类必须加上 GatewayFilterFactory 作为结尾。
  3. 必须继承 AbstractGatewayFilterFactory。
  4. 必须声明静态内部类,声明属性来接收配置文件中对应的断言的信息。
  5. 需要结合 shortcutFieldOrder 进行绑定。
  6. 重写 apply 方法。
@Component
public class RequestStatsGatewayFilterFactory extends AbstractGatewayFilterFactory<RequestStatsGatewayFilterFactory.Config> {

    public RequestStatsGatewayFilterFactory() {
        super(Config.class);
    }

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("service");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            Long count = redisTemplate.opsForValue().increment(config.service);
            System.out.println(count);
            return chain.filter(exchange);
        };
    }

    public static class Config {
        private String service;

        public Config() {
        }

        public String getService() {
            return service;
        }

        public void setService(String service) {
            this.service = service;
        }
    }
}

 9.3、配置路由过滤器

server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: user_route
          uri: lb://user-service
          predicates:
            - Path=/user-service/**
          filters:
            - StripPrefix=1
            - RequestStats=user-service # 自定义路由过滤器
  redis:
    host: localhost
    port: 6379

9.4、访问测试

在浏览器中多次访问 http://localhost:8050/user-service/user/findUserById/1

Redis 统计数据:

十、全局过滤器

全局过滤器:针对所有路由请求,一旦定义就会投入使用。

10.1、ReactiveLoadBalancerClientFilter

负载均衡过滤器,它主要是做两件事情:

  1. 判断请求路径是否 lb 开头
  2. 调用负载均衡器实现进行负载均衡的请求。
spring:    
  gateway:
    routes:
      - id: user_route
        uri: lb://user-service
        predicates:
          - Path=/user-service/**

十一、自定义全局过滤器

11.1、调用时长统计

假设我们现在需要统计每一次调用的处理时长。

我们看看在这个场景下,用自定义全局过滤器怎么来实现。

  • 引入 Lombok 依赖
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
  • 代码实现
@Slf4j
@Component
public class ProcessingTimeGlobalFilter implements GlobalFilter, Ordered {

    private static final String PROCESSING_START_TIME = "processingStartTime";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        exchange.getAttributes().put(PROCESSING_START_TIME, System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    Long startTime = exchange.getAttribute(PROCESSING_START_TIME);
                    if (startTime != null) {
                        long endTime = (System.currentTimeMillis() - startTime);
                        log.info("{}耗时: {}ms", exchange.getRequest().getPath(), endTime);
                    }
                })
        );
    }

    @Override
    public int getOrder() {
        // 最低优先级,在过滤器链中最晚执行,统计结果更接近微服务的处理时长。
        return Ordered.LOWEST_PRECEDENCE;
    }
}
  • 自定义全局过滤器不用配置在路由中

只要实现 GlobalFilter 接口,并声明为 Spring Bean (@Component),GatewayAutoConfiguration 自动装配的时候,就会把 List<GlobalFilter> 列表注入 FilteringWebHandler 以便后续处理。

public class GatewayAutoConfiguration {
    @Bean
    public FilteringWebHandler filteringWebHandler(List<GlobalFilter> globalFilters) {
        return new FilteringWebHandler(globalFilters);
    }
}
  • 适配器模式

FilteringWebHandler 持有的是 GatewayFilter 类型列表,而传入的是 GlobalFilter 类型列表。这里用到适配器模式。

// 实现目标接口 GatewayFilter 
private static class GatewayFilterAdapter implements GatewayFilter {
        // 持有真实接口实例
        private final GlobalFilter delegate;

        GatewayFilterAdapter(GlobalFilter delegate) {
            this.delegate = delegate;
        }

        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 请求委托给真实接口实例
            return this.delegate.filter(exchange, chain);
        }

        public String toString() {
            StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");
            sb.append("delegate=").append(this.delegate);
            sb.append('}');
            return sb.toString();
        }
}
  • 实现 Ordered 接口

因为过滤器是在过滤器链里面执行的,所以需要实现 Ordered 接口以决定它在过滤器链中的执行顺序。

11.2、Token 校验

前后端分离的 Token 校验过程: 

  • WebClientConfig
@Configuration
public class WebClientConfig {

    @Bean
    @LoadBalanced // 关键注解,启用负载均衡
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder();
    }
}

新版的 Gateway 已经全面接入 WebFlux,所以 gateway -> 后端认证服务 的调用最好使用 WebClient 这种非阻塞 API。  

  •  application.yml
server:
  port: 8050

spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        username: nacos
        password: nacos
        group: DEFAULT_GROUP
        server-addr: 127.0.0.1:8848
    gateway:
      routes:
        - id: user_route
          uri: lb://user-service
          predicates:
            - Path=/user-service/**
          filters:
            - StripPrefix=1
            - RequestStats=user-service
  redis:
    host: localhost
    port: 6379

custom: # 自定义不校验 token 路径
  token:
    ignore:
      paths:
        - /user-service/user/login
  •  自定义全局过滤器
@Slf4j
@Component
@ConfigurationProperties(prefix = "custom.token.ignore")
public class TokenValidateGlobalFilter implements GlobalFilter, Ordered {
    @Setter
    private List<String> paths;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    private final WebClient webClient;

    // 通过构造器注入WebClient
    public TokenValidateGlobalFilter(WebClient.Builder webClientBuilder) {
        this.webClient = webClientBuilder.baseUrl("http://user-service").build();
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        // 是否忽略路径:false-不忽略,true-忽略
        boolean ignorePath = this.ignorePath(path);
        if (ignorePath) {
            return chain.filter(exchange).ignoreElement();
        } else {
            // 1. 从请求头获取token
            String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
            // 2. 如果没有token,直接返回401
            if (token == null || !token.startsWith("Bearer ")) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            // 提取纯token(去掉Bearer前缀)
            String strToken = token.substring(7);
            // 3. 调用用户服务验证token
            return webClient.get()
                    .uri("/user/validateToken?token={token}", strToken)
                    .retrieve()
                    .bodyToMono(Boolean.class)
                    .flatMap(resp -> {
                        if (resp) {
                            // 4. token验证成功,继续过滤器链
                            return chain.filter(exchange);
                        } else {
                            // 5. token验证失败,返回401
                            ServerHttpResponse response = exchange.getResponse();
                            byte[] bits = HttpStatus.UNAUTHORIZED.getReasonPhrase().getBytes();
                            DataBuffer buffer = response.bufferFactory().wrap(bits);
                            response.setStatusCode(HttpStatus.UNAUTHORIZED);
                            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
                            return response.writeWith(Mono.just(buffer));
                        }
                    })
                    .onErrorResume(e -> {
                        log.error("报错", e);
                        // 6. 调用用户服务失败时的处理
                        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                        return exchange.getResponse().setComplete();
                    });
        }
    }

    /**
     * 是否忽略路径
     *
     * @param path 请求路径
     * @return false 不忽略,true 忽略
     */
    private boolean ignorePath(String path) {
        boolean result = false;
        for (String source : paths) {
            result = antPathMatcher.matchStart(source, path);
            if (result) {
                break;
            }
        }
        return result;
    }

    @Override
    public int getOrder() {
        return -1;
    }

}
  • UserController
    @RequestMapping("/validateToken")
    public ResponseEntity<Boolean> validateToken(@RequestParam String token) {
        // 这里实现你的token验证逻辑
        // 可以使用 JWT 或者任何你想要的方式
        // 这里简单演示,就写一个固定的UUID
        boolean isValid = "8216f4b7-8aa8-4a1c-84eb-4ab89d3b2785".equals(token);
        return ResponseEntity.ok(isValid);
    }
  • 访问测试

没有 token 时:

有 token 时:

十二、源码