Skip to content

网关 ​

tio-boot-cloud-gateway 把一个 tio-boot 应用变成 API 网关:客户端只连接网关,网关按路径把请求转发给后端服务。

  • 路由:Path、Method、Header、Query、Host、RemoteAddr 断言。
  • 过滤器:StripPrefix、PrefixPath、RewritePath、SetPath、增删请求头、添加响应头,以及自定义全局过滤器(鉴权、限流、审计等)。
  • 转发:lb://服务名 按负载均衡选实例,连接失败自动换实例;SSE 等流式响应边收边发;WebSocket 连接同样按路由转发。
  • 路由可以写在 app.properties,也可以放在 Nacos 中,修改后立即生效。

依赖 ​

xml
<dependency>
  <groupId>nexus.io</groupId>
  <artifactId>tio-boot-cloud-gateway</artifactId>
  <version>${tio-boot-cloud.version}</version>
</dependency>
<dependency>
  <groupId>nexus.io</groupId>
  <artifactId>tio-boot-cloud-nacos</artifactId>
  <version>${tio-boot-cloud.version}</version>
</dependency>

网关应用 ​

java
package nexus.io.tio.boot.cloud.demo.gateway;

import nexus.io.tio.boot.TioApplication;
import nexus.io.tio.boot.cloud.demo.gateway.config.GatewayConfig;

public class GatewayApp {
  public static void main(String[] args) {
    TioApplication.run(GatewayApp.class, new GatewayConfig(), args);
  }
}
java
package nexus.io.tio.boot.cloud.demo.gateway.config;

import nexus.io.context.BootConfiguration;
import nexus.io.tio.boot.cloud.demo.gateway.filter.BlockListFilter;
import nexus.io.tio.boot.cloud.gateway.TioBootGateway;
import nexus.io.tio.boot.cloud.nacos.TioBootNacos;

public class GatewayConfig implements BootConfiguration {

  @Override
  public void config() throws Exception {
    TioBootNacos.start();
    TioBootGateway.start();
    TioBootGateway.addGlobalFilter(new BlockListFilter());
  }
}

TioBootGateway.start() 读取路由,注册内置的 CORS 和 Trace Id 过滤器,并接管应用中没有其他处理器的全部 HTTP 路径和 WebSocket 路径;应用自己注册的路由(例如 /health)照常由自己处理。

路由 ​

properties
app.name=demo-gateway
server.port=18080
server.shutdown.hook=true

nacos.server-addr=127.0.0.1:8848
nacos.namespace=hula-dev
nacos.username=nacos
nacos.password=nacos
nacos.discovery.ip=127.0.0.1
nacos.config.data-ids=demo-gateway.yml

# /api/provider/demo/hello -> demo-provider 的 /demo/hello(HTTP、SSE、WebSocket)
gateway.routes[0].id=provider
gateway.routes[0].uri=lb://demo-provider
gateway.routes[0].predicates[0]=Path=/api/provider/**
gateway.routes[0].filters[0]=StripPrefix=2

# /api/consumer/consumer/hello -> demo-consumer 的 /consumer/hello
gateway.routes[1].id=consumer
gateway.routes[1].uri=lb://demo-consumer
gateway.routes[1].predicates[0]=Path=/api/consumer/**
gateway.routes[1].filters[0]=StripPrefix=2

每条路由:

  • id:路由名,用于日志。
  • uri:转发目标。lb://service 按服务名负载均衡;lb:ws://service 表示 WebSocket 服务(lb:// 的路由同样可以转发 WebSocket);也可以写固定地址 http://host:port、https://host 或 ws://host:port。
  • predicates[i]:断言,全部满足才命中。
  • filters[i]:过滤器,按顺序执行。
  • order:顺序,数字小的先匹配,默认 0。

断言 ​

  • Path=/api/user/**,/user/**:路径匹配任意一个模式。* 匹配一段路径中的任意字符,** 匹配任意多段,{id} 匹配一段;末尾的 /** 同时匹配 /api/user 本身。
  • Method=GET,POST
  • Header=x-version 或 Header=x-version, v2.*:请求头存在,或者匹配正则。
  • Query=debug 或 Query=type, \d+:查询参数存在,或者匹配正则。
  • Host=api.example.com,*.example.com
  • RemoteAddr=10.0.,192.168.1.10:客户端 IP 以其中之一开头。

过滤器 ​

  • StripPrefix=2:去掉前两段路径,并通过 x-forwarded-prefix 告诉后端去掉的前缀。
  • PrefixPath=/api:在路径前加前缀。
  • RewritePath=/old/(?<rest>.*), /new/${rest}:正则改写路径。
  • SetPath=/fixed:替换整个路径。
  • AddRequestHeader=x-from, gateway、SetRequestHeader=...、RemoveRequestHeader=cookie
  • AddResponseHeader=x-served-by, gateway

查询参数原样转发。转发时设置 X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host,后端可以拿到客户端真实 IP。

写在 YAML 中 ​

路由也可以写在 Nacos 的 demo-gateway.yml 中,发布后网关立即使用新路由,不需要重启:

yaml
gateway:
  routes:
    - id: provider
      uri: lb://demo-provider
      predicates:
        - Path=/api/provider/**
      filters:
        - StripPrefix=2
    - id: v2
      uri: lb://demo-provider
      predicates:
        - Path=/v2/**
      filters:
        - StripPrefix=1
shell
curl "localhost:18080/v2/demo/hello?name=hot"
{"data":{"instance":"127.0.0.1:18083","message":"Greetings v2, hot"},"code":1,"ok":true}

路由也可以在代码中添加,和配置中的路由一起生效:

java
TioBootGateway.addRoute(new RouteDefinition("docs", "http://127.0.0.1:8080")
    .predicate("Path=/docs/**").filter("StripPrefix=1"));

全局过滤器 ​

全局过滤器作用于所有路由,适合放鉴权、黑名单、审计。返回一个响应即可直接拒绝请求,调用 chain.filter(exchange) 继续转发。下面的过滤器拒绝 demo.blocked-tokens 中的 token:

java
package nexus.io.tio.boot.cloud.demo.gateway.filter;

import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.cloud.gateway.GatewayExchange;
import nexus.io.tio.boot.cloud.gateway.filter.GatewayFilterChain;
import nexus.io.tio.boot.cloud.gateway.filter.GlobalFilter;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.environment.EnvUtils;

/**
 * 拒绝 demo.blocked-tokens(逗号分隔)中的 token,HTTP 和 WebSocket 请求都生效
 */
public class BlockListFilter implements GlobalFilter {

  @Override
  public HttpResponse filter(GatewayExchange exchange, GatewayFilterChain chain) throws Exception {
    String token = exchange.getRequestHeader("token");
    String blocked = EnvUtils.get("demo.blocked-tokens", "");
    if (token != null && !token.isEmpty() && ("," + blocked + ",").contains("," + token + ",")) {
      HttpResponse response = exchange.getResponse();
      response.setStatus(401);
      return response.setJson(RespBodyVo.fail(401, "token blocked"));
    }
    return chain.filter(exchange);
  }
}
shell
curl -s -H 'token: bad' localhost:18080/api/provider/demo/hello
{"data":null,"code":401,"error":null,"ok":false,"msg":"token blocked"}

GatewayExchange 中可以读取请求、修改转发的路径(setPath)、设置或删除转发的请求头(setRequestHeader / removeRequestHeader)、设置返回给客户端的响应头(setResponseHeader),isWebSocket() 表示当前是 WebSocket 握手。getOrder() 决定执行顺序,数字小的先执行。

常见的用法是在网关解析登录态,把用户信息写成请求头传给后端,后端不再重复解析 token:

java
exchange.removeRequestHeader("userId");
exchange.setRequestHeader("userId", String.valueOf(session.getUserId()));

先删除客户端自带的同名请求头,后端就可以信任这些请求头。

内置过滤器 ​

  • CORS:回显请求的 Origin、允许携带凭据,直接响应预检请求。gateway.cors.allowed-origins 限制允许的来源,gateway.cors.enabled=false 关闭。
  • Trace Id:请求没有 x-trace-id 时生成一个雪花 ID,传给后端并返回给客户端,日志可以按它串起整条调用链。gateway.trace.header 修改请求头名称,gateway.trace.enabled=false 关闭。

SSE 与流式响应 ​

后端返回 text/event-stream 或没有长度的分块响应时,网关收到一段就发出一段,AI 对话等流式接口通过网关同样逐字输出:

shell
curl -N localhost:18080/api/provider/demo/stream
event:message
data:{"index":1,"instance":"127.0.0.1:18081"}
event:message
data:{"index":2,"instance":"127.0.0.1:18081"}
...

每条事件间隔 500 毫秒到达,与直接访问服务一致。读取超时由 gateway.http.read-timeout 控制,默认 300 秒。

WebSocket ​

WebSocket 握手按同样的路由匹配,全局过滤器也会执行(可以在握手时校验 token)。网关连接上后端后双向转发文本和二进制消息,任何一端断开时另一端随之关闭:

javascript
const ws = new WebSocket('ws://127.0.0.1:18080/api/provider/demo/ws');
ws.onmessage = (e) => console.log(e.data);  // connected to 127.0.0.1:18083
ws.onopen = () => ws.send('ping-1');         // echo: ping-1

在全局过滤器中拒绝的 token 在握手阶段就被拒绝,连接不会建立。

失败处理 ​

  • 没有匹配的路由:404,{"code":404,"msg":"No route for /path"}。
  • 服务没有可用实例:503。
  • 实例连接失败:自动换一个实例;都失败时 502。实例被强制停止后,在 Nacos 摘除它之前的请求同样都能成功。
properties
# 秒
gateway.http.connect-timeout=3
gateway.http.read-timeout=300
gateway.http.max-attempts=2

灰度 ​

和服务调用一样,配置按元数据选实例:

properties
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=version

请求头 gray_version: v2 的请求只转发给元数据 version=v2 的实例,并且原样传给后端,后端继续调用其他服务时也会选择 v2 实例。

MIT Licensed