网关
tio-boot-cloud-gateway 把一个 tio-boot 应用变成 API 网关:客户端只连接网关,网关按路径把请求转发给后端服务。
- 路由:Path、Method、Header、Query、Host、RemoteAddr 断言。
- 过滤器:StripPrefix、PrefixPath、RewritePath、SetPath、增删请求头、添加响应头,以及自定义全局过滤器(鉴权、限流、审计等)。
- 转发:
lb://服务名按负载均衡选实例,连接失败自动换实例;SSE 等流式响应边收边发;WebSocket 连接同样按路由转发。 - 路由可以写在
app.properties,也可以放在 Nacos 中,修改后立即生效。
依赖
<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>网关应用
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);
}
}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)照常由自己处理。
路由
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,POSTHeader=x-version或Header=x-version, v2.*:请求头存在,或者匹配正则。Query=debug或Query=type, \d+:查询参数存在,或者匹配正则。Host=api.example.com,*.example.comRemoteAddr=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=cookieAddResponseHeader=x-served-by, gateway
查询参数原样转发。转发时设置 X-Forwarded-For、X-Forwarded-Proto、X-Forwarded-Host,后端可以拿到客户端真实 IP。
写在 YAML 中
路由也可以写在 Nacos 的 demo-gateway.yml 中,发布后网关立即使用新路由,不需要重启:
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=1curl "localhost:18080/v2/demo/hello?name=hot"
{"data":{"instance":"127.0.0.1:18083","message":"Greetings v2, hot"},"code":1,"ok":true}路由也可以在代码中添加,和配置中的路由一起生效:
TioBootGateway.addRoute(new RouteDefinition("docs", "http://127.0.0.1:8080")
.predicate("Path=/docs/**").filter("StripPrefix=1"));全局过滤器
全局过滤器作用于所有路由,适合放鉴权、黑名单、审计。返回一个响应即可直接拒绝请求,调用 chain.filter(exchange) 继续转发。下面的过滤器拒绝 demo.blocked-tokens 中的 token:
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);
}
}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:
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 对话等流式接口通过网关同样逐字输出:
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)。网关连接上后端后双向转发文本和二进制消息,任何一端断开时另一端随之关闭:
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 摘除它之前的请求同样都能成功。
# 秒
gateway.http.connect-timeout=3
gateway.http.read-timeout=300
gateway.http.max-attempts=2灰度
和服务调用一样,配置按元数据选实例:
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=version请求头 gray_version: v2 的请求只转发给元数据 version=v2 的实例,并且原样传给后端,后端继续调用其他服务时也会选择 v2 实例。
