网关
tio-boot-cloud-gateway 把一个 tio-boot 应用变成 API 网关:客户端只连接网关,网关按路径把请求转发给后端服务。
- 路由:Path、Method、Header、Query、Host、RemoteAddr 断言。
- 过滤器:StripPrefix、PrefixPath、RewritePath、SetPath、增删请求头、添加响应头,以及自定义全局过滤器(鉴权、限流、审计等)。
- 转发:
lb://服务名按负载均衡选实例,连接失败自动换实例;SSE 等流式响应边收边发;WebSocket 连接同样按路由转发。 - 路由可以写在
app.properties,也可以放在 Nacos 中,修改后立即生效。
创建项目
tio-boot-cloud-nacos-gateway
依赖
<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
# 黑名单演示:请求头 token 的值为 bad 时返回 401
demo.blocked-tokens=bad
# /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,示例应用的 app.properties 中写的是 demo.blocked-tokens=bad,也可以放到 Nacos 的 demo-gateway.yml 中,修改后立即生效:
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 = tokenOf(exchange);
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);
}
/**
* 优先读请求头 token。浏览器的 EventSource 和 WebSocket 无法设置请求头,这时读查询参数 token
*/
private String tokenOf(GatewayExchange exchange) {
String token = exchange.getRequestHeader("token");
if (token == null || token.isEmpty()) {
token = exchange.getRequest().getParam("token");
}
return token;
}
}exchange.getRequestHeader("token") 读取客户端发来的请求头,名称不区分大小写。黑名单按逗号分隔、整值匹配:token: bad 命中,token: baddy 不命中。
curl -s -H 'token: bad' localhost:18080/api/provider/demo/hello
{"data":null,"code":401,"error":null,"ok":false,"msg":"token blocked"}Windows 的 cmd 不把单引号当作引号,上面的命令要写成双引号,否则 curl 收不到这个请求头,过滤器读到的 token 就是 null:
curl -s -H "token: bad" localhost:18080/api/provider/demo/helloPowerShell 5.1 中 curl 是 Invoke-WebRequest 的别名,要写 curl.exe 才能用 curl 的参数。请求头读不到时过滤器会继续读查询参数,所以浏览器里也可以用地址栏直接验证,EventSource 和 WebSocket 这类无法设置请求头的客户端同样适用:
curl -s "localhost:18080/api/provider/demo/hello?token=bad"
{"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熔断和降级
连接失败和读取超时计入失败。一条路由连续失败达到次数后进入熔断,这段时间内不再请求上游;时间到后放行一个请求试探,成功就恢复,失败就继续熔断。
gateway.circuit-breaker.enabled=true
# 连续失败多少次后熔断
gateway.circuit-breaker.failure-threshold=5
# 熔断持续的秒数
gateway.circuit-breaker.open-seconds=10熔断、没有可用实例或连接失败时,默认返回对应的状态码和一段 JSON。需要统一的响应格式时可以设置降级响应,TioBootGateway.start() 之后调用:
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.cloud.gateway.TioBootGateway;
TioBootGateway.setFallback((exchange, status, message) ->
exchange.getResponse().setJson(RespBodyVo.fail(status, "服务暂时不可用,请稍后再试")));灰度
和服务调用一样,配置按元数据选实例:
cloud.loadbalancer.metadata-header=gray_version
cloud.loadbalancer.metadata-key=version请求头 gray_version: v2 的请求只转发给元数据 version=v2 的实例,并且原样传给后端,后端继续调用其他服务时也会选择 v2 实例。
