Skip to main content

网关

网络的关口,负责请求的路由、转发、身份验证

Spring Cloud Gateway

基于SpringWebFlux技术,完全支持响应式编程,吞吐能力更强

  1. 创建独立的微服务,网关微服务gateway

  2. 导入相关依赖

    <dependencies>
    <!--common,这里是常用类-->
    <dependency>
    <groupId>com.heima</groupId>
    <artifactId>hm-common</artifactId>
    <version>1.0.0</version>
    </dependency>
    <!--网关-->
    <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>
    <build>
    <finalName>${project.artifactId}</finalName>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
  3. 配置启动类-GatewayApplication

    package com.xxx.gateway;

    @SpringBootApplication
    public class GatewayApplication {
    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class, args);
    }
    }
  4. 配置路由-application.yml

    server:
    port: 8080
    spring:
    application:
    name: gateway
    cloud:
    nacos:
    server-addr: 192.168.88.130:8848
    gateway:
    routes:
    - id: 微服务名称 # 路由规则id,自定义,唯一
    uri: lb://微服务名称 # 路由目标微服务,lb表示负载均衡
    predicates: # 路由断言,判断请求是否符合规则,符合则路由到目标
    - Path=/路由名/**, /路由名/**

路由属性

网关路由对应的Java类型是RouteDefinition

常见属性:

  • id:路由唯一标示
  • uri:路由目标地址
  • predicates:路由断言,判断请求是否符合当前路由
  • filters:路由过滤器,对请求或响应做特殊处理

路由断言-predicates

名称说明示例
After是某个时间点后的请求- After=2037-01-20T17:42:47.789-07:00[America/Denver]
Before是某个时间点之前的请求- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between是某两个时间点之前的请求- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求必须是访问某个host(域名)- Host=**.somehost.org,**.anotherhost.org
Method请求方式必须是指定方式- Method=GET,POST
Path请求路径必须符合指定规则- Path=/red/{segment},/blue/**
Query请求参数必须包含指定参数- Query=name, Jack或者- Query=name
RemoteAddr请求者的ip必须是指定范围- RemoteAddr=192.168.1.1/24
weight权重处理

路由过滤器

名称说明示例
addRequestHeader给当前请求添加一个请求头AddrequestHeader=headerName,headerValue
RemoveRequestHeader移除请求中的一个请求头RemoveRequestHeader=headerName
AddResponseHeader给响应结果中添加一个响应头AddResponseHeader=headerName,headerValue
RemoveResponseHeader从响应结果中移除一个响应头RemoveResponseHeader=headerName
RewritePath请求路径重写RewritePath=/red/?(?<segment>.*), /$\{segment}
StripPrefix去除请求路径中的N段前缀StripPrefix=1,则路径/a/b转发时只保留/b
……

网关登录校验

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。

  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。

  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前之后被执行。

  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。

  5. 微服务返回结果后,再倒序执行Filterpost逻辑。

  6. 最终把响应结果返回。

网关过滤器

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.

  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置。

GatewayFilterGlobalFilter这两种过滤器的方法签名完全一致:

/**
* 处理请求并将其传递给下一个过滤器
* @param exchange 当前请求的上下文,其中包含request、response等各种数据
* @param chain 过滤器链,基于它向下传递请求
* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

FilteringWebHandler在处理请求时,会将GlobalFilter装饰为GatewayFilter,然后放到同一个过滤器链中,排序以后依次执行。

Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route. 例如,有一个过滤器叫做AddRequestHeaderGatewayFilterFacotry,顾明思议,就是添加请求头的过滤器,可以给请求添加一个请求头并传递到下游微服务。 使用的使用只需要在application.yaml中这样配置:

spring:
cloud:
gateway:
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**
filters:
- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value

如果想要让过滤器作用于所有的路由,则可以这样配置:

spring:
cloud:
gateway:
default-filters: # default-filters下的过滤器可以作用于所有路由
- AddRequestHeader=key, value
routes:
- id: test_route
uri: lb://test-service
predicates:
-Path=/test/**

自定义网关过滤器

GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求
ServerHttpRequest request = exchange.getRequest();
// 编写过滤器逻辑
System.out.println("过滤器执行了");
// 放行
return chain.filter(exchange);
}
};
}
}

注意:该类的名称一定要以GatewayFilterFactory为后缀!

然后在yaml配置中这样使用:

spring:
cloud:
gateway:
default-filters:
- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器

另外,这种过滤器还可以支持动态配置参数,不过实现起来比较复杂,示例:

@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {

@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();
// 编写过滤器逻辑
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// 放行
return chain.filter(exchange);
}
}, 100);
}

// 自定义配置属性,成员变量名称很重要,下面会用到
@Data
static class Config{
private String a;
private String b;
private String c;
}
// 将变量名称依次返回,顺序很重要,将来读取参数时需要按顺序获取
@Override
public List<String> shortcutFieldOrder() {
return List.of("a", "b", "c");
}
// 返回当前配置类的类型,也就是内部的Config
@Override
public Class<Config> getConfigClass() {
return Config.class;
}

}

然后在yaml文件中使用:

spring:
cloud:
gateway:
default-filters:
- PrintAny=1,2,3 # 注意,这里多个参数以","隔开,将来会按照shortcutFieldOrder()方法返回的参数顺序依次复制

上面这种配置方式参数必须严格按照shortcutFieldOrder()方法的返回参数名顺序来赋值。

还有一种用法,无需按照这个顺序,就是手动指定参数名:

spring:
cloud:
gateway:
default-filters:
- name: PrintAny
args: # 手动指定参数名,无需按照参数顺序
a: 1
b: 2
c: 3

GlobalFilter

自定义GlobalFilter则简单很多,直接实现GlobalFilter即可,而且也无法设置动态参数:

@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 编写过滤器逻辑
System.out.println("未登录,无法访问");
// 放行
// return chain.filter(exchange);

// 拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}

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

配置管理

配置共享

设置NACOS的配置列表

例子:

spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto

这里的jdbc的相关参数并没有写死,例如:

  • 数据库ip:通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101,同时允许通过${hm.db.host}来覆盖默认值

  • 数据库端口:通过${hm.db.port:3306}配置了默认值为3306,同时允许通过${hm.db.port}来覆盖默认值

  • 数据库database:可以通过${hm.db.database}来设定,无默认值

拉取共享配置

读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml

SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件,将nacos地址配置到bootstrap.yaml中,在项目引导阶段就可以读取nacos中的配置了。

  1. 引入依赖

    <!--nacos配置管理-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--读取bootstrap文件-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
  2. 新建bootstrap.yaml,与application.yaml同级

    spring:
    application:
    name: cart-service # 服务名称
    profiles:
    active: dev
    cloud:
    nacos:
    server-addr: IP:8848 # nacos地址
    config:
    file-extension: yaml # 文件后缀名
    shared-configs: # 共享配置
    - dataId: shared-jdbc.yaml # 共享mybatis配置
    - dataId: ……
  3. 修改application.yaml

    server:
    port: 8082
    feign:
    okhttp:
    enabled: true # 开启OKHttp连接池支持
    hm:
    swagger:
    title: "标题"
    package: com.xxx.xx.controller # controller包
    db:
    database: xxx # 数据库名

配置热更新

当修改配置文件中的配置时,微服务无需重启即可使配置生效

前提:

  • nacos中要有一个与微服务名有关的配置文件

    spring.application.name-spring.active.profile.file-extension
    微服务名称-项目profile(可选).文件后缀名
    user-service-dev.yaml

    cart-service.yaml

    hm:
    cart:
    maxItems: 1
  • 微服务中要以特定方式读取需要热更新的配置属性, cart.config包下

    • @Data
      @ConfigurationProperties(prefix = "hm.cart")
      public class CartProperties {
      private int maxItems;
      }

    • @Data
      @RefreshScope
      public class CartProperties {
      @Value("${hm.cart.maxItems}")
      private int maxItems;
      }

动态路由

将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息

监听Nacos配置变更

Nacos官网中给出了手动监听Nacos配置变更的SDK:https://nacos.io/zh-cn/docs/sdk.html

示例,网关微服务下:

  1. 引入依赖

    <!--nacos配置管理-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!--读取bootstrap文件-->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>

    配置bootstrap.yaml文件

    spring:
    application:
    name: gateway
    profiles:
    active: dev
    cloud:
    nacos:
    server-addr: 192.168.88.130:8848
    config:
    file-extension: yaml
    shared-configs:
    - data-id: shared-log.yaml

    修改对应的application.yaml

    server:
    port: 8080
    hm:
    jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
    auth:
    excludePaths:
    - /search/**
    - /users/login
    - /items/**
    - /hi
  2. 创建类编写监听器

    @PostConstruct:在项目初始化后执行装饰的类

    @Component
    @Slf4j
    @RequiredArgsConstructor
    public class DynamicRouteLoader {

    private final NacosConfigManager nacosConfigManager;
    private final RouteDefinitionWriter writer;

    private final String dataId = "gateway-routes.json";
    private final String group = "DEFAULT_GROUP";
    private final Set<String> routeIds = new HashSet<>();

    @PostConstruct
    public void initRouteConfigListener() throws NacosException {
    // 1.项目启动,先拉取配置,添加配置监听器
    String configInfo = nacosConfigManager.getConfigService()
    .getConfigAndSignListener(dataId, group, 5000, new Listener() {
    @Override
    public Executor getExecutor() {
    return null;
    }

    @Override
    public void receiveConfigInfo(String configInfo) {
    // 2.监听配置变更,更新路由表
    updateConfigInfo(configInfo);
    }
    });
    // 3.监听初始配置,更新路由表
    updateConfigInfo(configInfo);
    }

    // 解析JSON信息
    public void updateConfigInfo(String configInfo) {
    log.debug("监听到路由配置信息:{}", configInfo);
    // 1.解析配置信息,转为RouteDefinition
    List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
    // 2.删除旧的路由表
    for (String routeId : routeIds) {
    writer.delete(Mono.just(routeId)).subscribe();
    }
    routeIds.clear();
    // 3.更新路由表
    for (RouteDefinition routeDefinition : routeDefinitions) {
    writer.save((Mono.just(routeDefinition))).subscribe();
    // 记录路由id
    routeIds.add(routeDefinition.getId());
    }
    }
    }

    Nacos里的配置文件的格式

    [
    {
    "id": "路由id,唯一标识",
    "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/路由名/**", "_genkey_1":"/路由名/**"}
    }],
    "filters": [],
    "uri": "lb://微服务名称"
    },
    {……}
    ]