路由网关统一访问接口

什么是 Spring Cloud Gateway

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。
15087669-025713559ec524fb

Spring Cloud Gateway 功能特征

  • 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
  • 动态路由
  • Predicates 和 Filters 作用于特定路由
  • 集成 Hystrix 断路器
  • 集成 Spring Cloud DiscoveryClient
  • 易于编写的 Predicates 和 Filters
  • 限流
  • 路径重写

Spring Cloud Gateway 工程流程

15087669-5e9ee4de2d7257bb
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或之后(post)执行业务逻辑。

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.wsl</groupId>
        <artifactId>hello-spring-cloud-alibaba-dependencies</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>iot-gateway</artifactId>
    <name>iot-gateway</name>
    <description>网关</description>

    <dependencies>
        <!-- Spring Boot Begin -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!-- Spring Cloud Begin -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- Feign Client for loadBalancing -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!-- Spring Cloud End -->

        <!-- Commons Begin -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
        <!-- Commons Begin -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

主要增加了 org.springframework.cloud:spring-cloud-starter-gateway 依赖

Application

package com.huitu.iot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class IotGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(IotGatewayApplication.class, args);
    }
}

application.yml

spring:
  application:
    # 应用名称
    name: iot-gateway
  cloud:
    # 使用 Naoos 作为服务注册发现
    nacos:
      discovery:
        #nacos注册地址
        server-addr: localhost:8180
        #注册服务的ip,默认是内网ip,当只需要内网访问时可以注释掉
        ip: localhost
        #命名空间
        namespace: 5709f890-96d1-42ed-b2ca-sdafewqasdf
    # 使用 Sentinel 作为熔断器
    sentinel:
      transport:
        port: 18721
        dashboard: localhost:18080
    # 路由网关配置
    gateway:
      # 设置与服务注册发现组件结合,这样可以采用服务名的路由策略
      discovery:
        locator:
          enabled: true
      # 配置路由规则
      routes:
        # 采用自定义路由 ID(有固定用法,不同的 id 有不同的功能,详见:https://cloud.spring.io/spring-cloud-gateway/2.0.x/single/spring-cloud-gateway.html#gateway-route-filters)
        - id: IOT-CONSUMER
          # 采用 LoadBalanceClient 方式请求,以 lb:// 开头,后面的是注册在 Nacos 上的服务名
          uri: lb://iot-consumer
          # Predicate 翻译过来是“谓词”的意思,必须,主要作用是匹配用户的请求,有很多种用法
          predicates:
            # Method 方法谓词,这里是匹配 GET 和 POST 请求
            - Method=GET,POST
server:
  port: 18087

# 目前无效
feign:
  sentinel:
    enabled: true

# 目前无效
management:
  endpoints:
    web:
      exposure:
        include: "*"

# 配置日志级别,方便调试
logging:
  level:
    org.springframework.cloud.gateway: debug

依次运行 Nacos 服务:iot-provider、iot-consumer、iot-gateway

打开浏览器访问:http://localhost:18087/iot-consumer/test/hi 浏览器显示

Hello Nacos Discovery Hi Feign i am from port 18085

注意:请求方式是 http://路由网关IP:路由网关Port/服务名/

至此说明 Spring Cloud Gateway 的路由功能配置成功

Spring Cloud Gateway 支持两种不同的用法:

编码式
properties、yml 配置

编码

项目创建成功后,直接配置一个 RouteLocator 这样一个 Bean,就可以实现请求转发。

@Component
public class GatewayController {
    //编码式转发
    @Bean
    RouteLocator routeLocator(RouteLocatorBuilder builder){
        return builder.routes().
                route("gongjie",j -> j.path("/get").uri("http://httpbin.org"))
                .build();
    }
}

这里只需要提供 RouteLocator 这个 Bean,就可以实现请求转发。配置完成后,重启项目,访问:http://localhost:18087/get

配置

properties

spring.cloud.gateway.routes[0].id=gongjie
spring.cloud.gateway.routes[0].uri=http://httpbin.org
spring.cloud.gateway.routes[0].predicates[0]=Path=/get

yml

spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Path=/get

访问:http://localhost:18087/get 实现同样的效果

Predicate

route的组成部分:
id:路由的ID
uri:匹配路由的转发地址
predicates:配置该路由的断言,通过PredicateDefinition类进行接收配置。
order:路由的优先级,数字越小,优先级越高。

spring cloud gateway 通过谓词(Predicate)来匹配来自用户的请求

  • After 通过时间匹配:
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - After=2022-03-17T01:01:01+08:00[Asia/Shanghai]

表示,请求时间在 2022-03-17T01:01:01+08:00[Asia/Shanghai] 时间之后,才会被路由。
除了 After 之外,还有两个关键字:
Before,表示在某个时间点之前进行请求转发
Between,表示在两个时间点之间,两个时间点用 , 隔开

  • Method 通过请求方式匹配:
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Method=GET

这个配置表示只给 GET 请求进行路由

  • Path 通过请求路径匹配:(重点)
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Path=/get

表示路径满足 get这个规则,都会被进行转发到http://httpbin.org/get
比如:http://localhost:18087/get

  • Query 通过参数进行匹配:
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Query=name

表示请求中一定要有 name 参数才会进行转发,否则不会进行转发。
也可以指定参数和参数的值。
例如参数的 key 为 name,value 必须要以 java 开始

spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Query=name,java.*
  • Header 通过请求头匹配
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Header=requestId

表示请求头中一定要有 requestId 参数才会进行转发,否则不会进行转发。

  • RemoteAddr 通过ip地址匹配
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - RemoteAddr=47.105.198.54/24
  • host 通过host匹配
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Host=**.gongjie.top
  • Cookie 通过Cookie匹配
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Cookie=CookieName, .*jie.*

这里判断cookie的 CookieName是否包含"jie" 是的话就路由到对应的服务上去

  • 组合使用
    当它们同时存在于同一个路由时,请求必须同时满足所有的谓词条件才被这个路由匹配。
    注意:一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发
spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: http://httpbin.org
          predicates:
            - Query=name,java.*
            - Method=GET
            - After=2021-01-01T01:01:01+08:00[Asia/Shanghai]

Filter

Spring Cloud Gateway 中的过滤器分为两大类:

  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上。
    20816836-9ee54ea5678e1362

AddRequestParameter 过滤器使用:

spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: lb://APPUSER
          filters:
            - AddRequestParameter=name,gong
          predicates:
            - Cookie=CookieName, .*jie.*

自动带上了参数name并且值为gong。

如果你有多个服务需要进行转发,配置多个routes规则就行

spring:
  cloud:
    gateway:
      routes:
        - id: gongjie
          uri: lb://APPUSER
          filters:
            - AddRequestParameter=name,gong
          predicates:
            - Cookie=CookieName, .*jie.*
        - id: yuanj
          uri: lb://APPPAY
          filters:
            - AddRequestParameter=name,Pay
          predicates:
            - Method=GET

一个转发到APPUSER,一个转发到APPPAY。

自定义GatewayFilter

/**
 * 统计某个或者某种路由的处理时长
 */
@Component
public class customGatewayFilter implements GatewayFilter, Ordered {
    private static final Logger log = LoggerFactory
            .getLogger( customGatewayFilter.class );
    private static final String COUNT_START_TIME = "countStartTime";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //得到当前时间
        Instant now = Instant.now();
        //Instant.now()使用的是UTC时间,会与北京时间相差八小时
        now.plusMillis(TimeUnit.HOURS.toMillis(8));
        //now.toEpochMilli()  毫秒数
        exchange.getAttributes().put(COUNT_START_TIME, now.toEpochMilli());
        return chain.filter(exchange).then(
                Mono.fromRunnable(()->{
                    long startTime = exchange.getAttribute(COUNT_START_TIME);
                    long endTime = (Instant.now().plusMillis(TimeUnit.HOURS.toMillis(8)).toEpochMilli() - startTime);
                    log.info(exchange.getRequest().getURI().getRawPath() + ": " + endTime + "ms");
                })
        );
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

上述代码中,getOrder()方法是来给过滤器设定优先级别的,值越大则优先级越低。需要将自定义的GatewayFilter 注册到router中,代码如下:

 @Bean
    RouteLocator routeLocator(RouteLocatorBuilder builder){
        return builder.routes()
                .route("customGongj",
                        j -> j.path("/get").filters(f -> f.filter(new CustomGatewayFilter())
                                .addRequestHeader("auth","gongjie")).uri("http://httpbin.org")
                )
                .build();
    }

访问http://localhost:18087/get 可看到效果,控制台有日志输出

2022-03-17 15:00:28.691  INFO 10364 --- [ctor-http-nio-4] com.gongj.gateway.CustomGatewayFilter    : /get: 28801395ms

自定义过滤器工厂

自定义GatewayFilter又有两种实现方式,一种是上面的直接实现GatewayFilter接口,另一种是自定义过滤器工厂(继承AbstractGatewayFilterFactory类) , 选择自定义过滤器工厂的方式,可以在配置文件中配置过滤器了。

@Component
public class CustomerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomerGatewayFilterFactory.Config> {
    private static final Logger log = LoggerFactory.getLogger( CustomerGatewayFilterFactory.class );
    private static final String COUNT_START_TIME = "countStartTime";
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }
            exchange.getAttributes().put(COUNT_START_TIME, System.currentTimeMillis());
            return chain.filter(exchange).then(
                    Mono.fromRunnable(() -> {
                        Long startTime = exchange.getAttribute(COUNT_START_TIME);
                        if (startTime != null) {
                            StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
                                    .append(": ")
                                    .append(System.currentTimeMillis() - startTime)
                                    .append("ms");
                            sb.append(" params:").append(exchange.getRequest().getQueryParams());
                            log.info(sb.toString());
                        }
                    })
            );
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("enabled");
    }

    public CustomerGatewayFilterFactory() {
        super(Config.class);
        log.info("Loaded GatewayFilterFactory [CustomerGatewayFilterFactory]");
    }
    public static class Config {
        /**
         * 控制是否开启统计
         */
        private boolean enabled;

        public Config() {}

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }
}

application.yml 中 网关路由配置如下:

spring:
  cloud:
    gateway:
      routes:
        - id: customer_route
          uri: http://httpbin.org
          filters:
            - Customer=true
            - AddRequestHeader=auth,gongjie123
          predicates:
            - Method=GET

访问 http://localhost:18087/get 可看到headers里面自动加入了"Auth": "gongjie123", 控制台打印:

2022-03-17 15:25:59.705  INFO 12836 --- [ctor-http-nio-3] c.g.g.CustomerGatewayFilterFactory       : /get: 437ms params:{}

Global filter

Spring Cloud Gateway框架内置的GlobalFilter如下:
20816836-6feeb6341847ede1
内置的 GlobalFilter 能够满足大多数的需求了,但是如果遇到特殊情况,内置满足不了我们的需求,还可以自定义GlobalFilter。

自定义GlobalFilter

下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。

/**
 * 鉴权过滤器
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");

        if (token == null || token.isEmpty()) {
            ServerHttpResponse response = exchange.getResponse();

            // 封装错误信息
            Map<String, Object> responseData = Maps.newHashMap();
            responseData.put("code", 401);
            responseData.put("message", "非法请求");
            responseData.put("cause", "Token is empty");

            try {
                // 将信息转换为 JSON
                ObjectMapper objectMapper = new ObjectMapper();
                byte[] data = objectMapper.writeValueAsBytes(responseData);

                // 输出错误信息到页面
                DataBuffer buffer = response.bufferFactory().wrap(data);
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
                return response.writeWith(Mono.just(buffer));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }
        return chain.filter(exchange);
    }

    /**
     * 设置过滤器的执行顺序
     *
     * @return
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }


}

访问 http://localhost:18087/get 将返回

 {"code":401,"cause":"Token is empty","message":"非法请求"}

加上token可正常访问 http://localhost:18087/get?token=1

参考链接:Gateway的使用

Q.E.D.