starapi-gateway 开放接口网关
开放 API 的独立调用入口:外部调用方只访问网关,网关完成鉴权、限流、幂等、转发、扣费与调用日志采集。
网关是独立 Spring Boot 应用(Jetty 容器),不连接业务库,路由/凭证/授权全部来自 Redis 缓存,
扣费与调用日志通过内网 HTTP 回传给 starapi-admin。
一、模块定位与调用链
外部调用方
│ /api/v1/**(X-App-Key / X-Timestamp / X-Nonce / X-Signature)
▼
┌──────────────────────────────────────────────┐
│ starapi-gateway (8082) │
│ 1. route : openapi:route:path 匹配 → openapi:route
│ 2. 接口维护中拦截 │
│ 3. 应用凭证 → 应用状态 → 开发者账号状态 │
│ 4. IP 白名单 │
│ 5. 时间戳偏移 → nonce 防重放 → 签名校验 │
│ 6. 接口授权(openapi:app:auth:{appId}) │
│ 7. 请求ID幂等(回放 or 拒绝) │
│ 8. QPS 令牌桶 + 应用日调用限额 │
│ 9. 计费预检(免费额度/次数包/钱包余额) │
│ 10. 转发上游(api_info.upstream_url) │
│ 11. 上游成功(2xx)后扣费 → 回写响应(含余额头) │
│ 12. 异步写调用日志 │
└───────┬──────────────────────────┬────────────┘
│ Redis 只读缓存 │ 内网 HTTP(X-Internal-Token)
▼ ▼
openapi 发布的缓存 starapi-admin (8080)
/internal/billing/precheck
/internal/billing/deduct
/internal/calllog/record
- 入口路径:
gateway.include-paths(默认仅/api/v1/**)匹配的请求才进入网关流程,其余请求直接交给后续过滤器与 MVC。 - 过滤器链:链路按阶段拆成 9 个
OncePerRequestFilter,由类上的@Order注解排序(顺序常量见GatewayConstants.FILTER_ORDER_*,以 10 为步长,计费预检插入在 165 位于限流与转发之间),任一阶段失败即写出统一错误响应并结束链路:
| 顺序 | 过滤器 | 职责 |
|---|---|---|
| 100 | OpenApiContextFilter |
建立请求上下文(requestId/方法/规范化路径/IP/请求体/开始时间);位于最外层,结束后统一记录调用日志 |
| 110 | OpenApiRouteFilter |
路由缓存匹配、接口维护中拦截 |
| 120 | OpenApiCredentialFilter |
应用凭证、应用状态、开发者账号状态、IP 白名单 |
| 130 | OpenApiSignFilter |
时间戳偏移、nonce 防重放、HMAC-SHA256 签名校验 |
| 140 | OpenApiAuthFilter |
应用对接口的授权校验 |
| 150 | OpenApiIdempotencyFilter |
请求ID幂等:命中缓存回放、并发请求抢占处理锁 |
| 160 | OpenApiRateLimitFilter |
QPS 令牌桶、应用日调用限额 |
| 165 | OpenApiBillingFilter |
转发前扣费预检:余额/次数包/免费额度均不可用时直接返回 40201,不调用上游 |
| 170 | OpenApiForwardFilter |
转发上游、2xx 且业务码 200 后扣费、回写响应(含账户余额头)、缓存成功响应;扣费失败时返回 40201/50002 错误响应且不回写上游结果 |
- 公共逻辑(路径判定、上下文读取、统一错误响应、异常信息整理)收敛在
AbstractOpenApiFilter,各阶段过滤器只实现自身校验。 - 网关错误统一返回 JSON:
{"code":业务码,"msg":"提示","requestId":"链路ID","timestamp":毫秒时间戳}。 - 请求ID(
X-Request-Id)同时是幂等键、扣费幂等键与调用日志主键;调用方不传或格式非法(超过 64 字符、含字母数字下划线中划线以外的字符)时由网关生成 32 位无横线 UUID。
二、请求头约定
| 请求头 | 必填 | 说明 |
|---|---|---|
X-App-Key |
是 | 应用 AppKey,用于读取应用凭证缓存;缺失或未命中返回 40101 |
X-Timestamp |
是 | 毫秒级时间戳字符串,默认允许与服务器时间相差 ±300 秒,超出返回 40102 |
X-Nonce |
是 | 请求随机串(建议 16~32 位),同一 AppKey 下只允许使用一次,重复返回 40103 |
X-Signature |
是 | 签名,64 位小写十六进制 HMAC-SHA256,见第三节;缺失或校验失败返回 40104 |
X-Request-Id |
否 | 链路追踪ID;缺省由网关生成,用于幂等、扣费与日志关联 |
Content-Type |
按接口 | 转发时原样传给上游;请求体按原始字节参与签名,GET 等空请求体按空数组处理 |
网关在签名校验通过、开始转发时还会向上游追加以下请求头,调用方无需也不能伪造:
| 转发头 | 说明 |
|---|---|
X-Request-Id |
本次调用的链路追踪ID(调用方未传时为网关生成值) |
X-Gateway-App-Id |
调用方应用ID |
X-Gateway-User-Id |
调用方所属开发者用户ID |
X-Gateway-Client-Ip |
网关解析出的调用方真实IP |
鉴权头与 HTTP 逐跳头(X-App-Key、X-Timestamp、X-Nonce、X-Signature、Host、Content-Length、Connection 等)不会透传给上游。
网关附加响应头
除原样回传上游响应头外,网关还会追加以下自有响应头(上游返回的同名头会被丢弃,避免伪造):
| 响应头 | 出现时机 | 说明 |
|---|---|---|
X-Gateway-Balance |
上游返回 2xx 且扣费结果带账户余额 | 本次调用扣费后的账户余额(单位:元,保留 4 位小数)。免费额度、次数包、钱包余额三种扣费方式均返回;扣费失败、上游非 2xx 或开发者未开户时不返回 |
X-Gateway-Replay |
命中请求ID幂等回放 | 固定为 true,表示本次未重复调用上游、未重复扣费;此时 X-Gateway-Balance 为首次调用扣费后的余额快照 |
扣费失败时不再返回上游结果,也不使用告警头:网关直接返回
40201(账户不可支付)或50002(计费接口异常)错误响应,调用方重试会重新调用上游。旧版本用于提示扣费失败的X-Gateway-Warning: charge-failed响应头已取消。账户余额取自扣费接口返回的balanceAfter,网关不额外查询钱包;转发前预检本身只读,不改变余额。浏览器跨域调用时需服务端通过Access-Control-Expose-Headers放行上述头才能被 JS 读取。
三、签名算法
签名实现见 com.starsee.starapi.gateway.util.OpenApiSignUtils,算法固定、区分大小写,调用方必须完全一致。
3.1 签名串
signContent = appKey + "\n"
+ timestamp + "\n"
+ nonce + "\n"
+ METHOD + "\n"
+ path + "\n"
+ md5Hex(body)
signature = hex( HMAC-SHA256( key = appSecret, data = signContent ) ) // 小写十六进制
| 段 | 规则 |
|---|---|
appKey |
请求头 X-App-Key 原值 |
timestamp |
请求头 X-Timestamp 原值(毫秒时间戳字符串,不重新格式化) |
nonce |
请求头 X-Nonce 原值 |
METHOD |
HTTP 方法,统一大写(get → GET) |
path |
请求路径,不含查询参数;以 / 开头,末尾多余的 / 会被去掉(根路径 / 保留);即 /api/v1/hello?x=1 参与签名的是 /api/v1/hello。为兼容按原始 URI 签名的调用方,网关会先用规范化路径校验,失败后再用保留尾斜杠的原始路径校验一次 |
body |
原始请求体字节,空请求体按空数组计算;空数组的 MD5 为 d41d8cd98f00b204e9800998ecf8427e |
补充规则:
appSecret作为 HMAC 密钥,按 UTF-8 编码;签名结果按 小写十六进制 传输,大小写不匹配视为失败。- 各段之间只用一个
\n分隔,末段(MD5)之后没有换行。 - 网关先校验时间戳、再占用 nonce、最后校验签名;签名失败时 nonce 已被占用,用同一 nonce 重试会得到
40103,重试请更换 nonce。 - 签名开关
gateway.sign.enabled=false时整段校验(时间戳/nonce/签名)全部跳过,仅用于临时排查。
3.2 curl + openssl 示例
export APP_KEY='your_app_key'
export APP_SECRET='your_app_secret'
export METHOD='POST'
export API_PATH='/api/v1/example/hello'
export BODY='{"name":"starapi"}'
TS=$(date +%s%3N) # 毫秒时间戳
NONCE=$(openssl rand -hex 16)
BODY_MD5=$(printf '%s' "$BODY" | openssl dgst -md5 -r | cut -d' ' -f1)
SIGN_CONTENT="${APP_KEY}
${TS}
${NONCE}
${METHOD}
${API_PATH}
${BODY_MD5}"
SIGNATURE=$(printf '%s' "$SIGN_CONTENT" | openssl dgst -sha256 -hmac "$APP_SECRET" -r | cut -d' ' -f1)
curl -sS -X "$METHOD" "http://127.0.0.1:8082${API_PATH}" \
-H 'Content-Type: application/json' \
-H "X-App-Key: ${APP_KEY}" \
-H "X-Timestamp: ${TS}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Signature: ${SIGNATURE}" \
-H "X-Request-Id: $(openssl rand -hex 16)" \
--data-binary "$BODY"
printf '%s' "$SIGN_CONTENT"会完整保留换行;签名串必须严格是 6 段、5 个\n。
3.3 Java(Hutool HMac)示例
网关与调用方使用同一套 Hutool API,可直接对照 OpenApiSignUtils#sign:
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.UUID;
String appKey = "your_app_key";
String appSecret = "your_app_secret";
String method = "POST";
String path = "/api/v1/example/hello";
byte[] body = "{\"name\":\"starapi\"}".getBytes(StandardCharsets.UTF_8);
String timestamp = String.valueOf(System.currentTimeMillis()); // 毫秒
String nonce = UUID.randomUUID().toString().replace("-", "");
String signContent = appKey + "\n"
+ timestamp + "\n"
+ nonce + "\n"
+ method.toUpperCase(Locale.ROOT) + "\n"
+ path + "\n"
+ DigestUtil.md5Hex(body);
HMac hmac = new HMac(HmacAlgorithm.HmacSHA256, appSecret.getBytes(StandardCharsets.UTF_8));
String signature = hmac.digestHex(signContent); // 小写十六进制
四、错误码
业务码与 HTTP 状态码同时返回,来源 GatewayErrorCode。
| 业务码 | HTTP | 枚举 | 含义 |
|---|---|---|---|
| 40101 | 401 | APP_CREDENTIAL_INVALID |
应用凭证无效,请检查AppKey |
| 40102 | 401 | TIMESTAMP_INVALID |
请求时间戳无效或已超出允许的时间偏移 |
| 40103 | 401 | NONCE_REPLAYED |
请求nonce已被使用,请勿重复提交 |
| 40104 | 401 | SIGNATURE_INVALID |
请求签名校验未通过(缺参数时提示“请求缺少签名参数”) |
| 40201 | 402 | BILLING_NOT_PAYABLE |
账户余额或可用次数不足:转发前预检拒绝,或预检后扣费时确认不可支付;msg 为具体原因(余额不足/仅支持套餐包/钱包冻结等) |
| 40301 | 403 | APP_DISABLED |
应用已被禁用 |
| 40302 | 403 | USER_DISABLED |
开发者账号已被停用或冻结 |
| 40303 | 403 | IP_NOT_ALLOWED |
调用来源IP不在应用白名单内 |
| 40304 | 403 | API_NOT_AUTHORIZED |
应用未开通该接口 |
| 40401 | 404 | ROUTE_NOT_FOUND |
开放接口不存在或已下线 |
| 40901 | 409 | REQUEST_DUPLICATE |
相同的请求ID正在处理或已完成,请勿重复提交 |
| 42901 | 429 | RATE_LIMITED |
调用频率超过接口QPS限制 |
| 42902 | 429 | DAILY_LIMIT_EXCEEDED |
调用次数已超过应用日调用限额 |
| 50001 | 500 | GATEWAY_ERROR |
网关内部错误 |
| 50002 | 500 | BILLING_FAILED |
上游已调用但本次扣费未完成(内网计费接口异常/超时等),不返回上游结果,可重试 |
| 50201 | 502 | UPSTREAM_ERROR |
上游服务调用失败 |
| 50301 | 503 | API_MAINTAIN |
开放接口维护中,请稍后重试 |
| 50401 | 504 | UPSTREAM_TIMEOUT |
上游服务响应超时 |
共 18 个业务码,全部来自 GatewayErrorCode。按 HTTP 状态可做通用重试判断,按业务码可精确定位原因。
五、配置说明
配置前缀 gateway,对应 GatewayProperties;application.yml 已给出默认值,application-dev.yml / application-prod.yml 只覆盖 Redis 与 admin 地址。
| 配置项 | 默认值 | 说明 |
|---|---|---|
gateway.enabled |
true |
网关过滤器总开关;关闭后 /api/v1/** 不再进入网关流程 |
gateway.include-paths |
/api/v1/** |
进入网关鉴权与转发的路径匹配规则(Spring PathPattern) |
gateway.sign.enabled |
true |
时间戳/nonce/签名校验开关 |
gateway.sign.timestamp-skew-seconds |
300 |
时间戳允许偏移(秒) |
gateway.sign.nonce-ttl-seconds |
300 |
nonce 防重放缓存时长(秒),应不小于时间戳偏移 |
gateway.rate-limit.enabled |
true |
QPS 限流开关 |
gateway.rate-limit.qps-interval-seconds |
1 |
令牌桶统计窗口(秒),窗口内允许请求数即 QPS 上限 |
gateway.rate-limit.daily-limit-enabled |
true |
应用日调用限额开关 |
gateway.idempotent.enabled |
true |
请求ID幂等开关 |
gateway.idempotent.lock-ttl-seconds |
60 |
“处理中”锁时长(秒) |
gateway.idempotent.response-ttl-seconds |
600 |
成功响应回放缓存时长(秒) |
gateway.idempotent.max-response-cache-bytes |
262144 |
可回放响应体上限(256KB),超出只去重不回放 |
gateway.billing.precheck-enabled |
true |
转发前扣费预检开关;关闭后恢复“先转发、后扣费、扣费失败仅告警”的旧行为 |
gateway.internal.admin-base-url |
http://127.0.0.1:8080 |
admin 内网访问地址 |
gateway.internal.token |
空(application.yml 取 ${INTERNAL_API_TOKEN:starapi-internal-token}) |
内网令牌,必须与 admin 的 internal.api.token 一致 |
gateway.internal.connect-timeout-ms |
2000 |
内网接口连接超时(毫秒) |
gateway.internal.read-timeout-ms |
5000 |
内网接口读超时(毫秒) |
gateway.internal.deduct-path |
/internal/billing/deduct |
扣费接口路径(取 InternalApiConstants) |
gateway.internal.precheck-path |
/internal/billing/precheck |
预检接口路径(取 InternalApiConstants) |
gateway.internal.call-log-path |
/internal/calllog/record |
调用日志接口路径(取 InternalApiConstants) |
gateway.internal.call-log-pool-size |
2 |
调用日志异步线程数(最大线程数不小于 4) |
gateway.internal.call-log-queue-capacity |
1024 |
调用日志异步队列容量,队列满由调用线程兜底执行 |
gateway.forward.connect-timeout-ms |
2000 |
上游连接超时(毫秒);读超时按接口 api_info.timeout_ms,未配置时默认 5000ms |
限流阈值不在网关配置里:QPS 取「应用 api_app.qps_limit(默认 50)」与「接口 api_info.qps_limit(默认 100)」的较小值;日限额取应用 api_app.daily_call_limit,未配置(≤0)表示不限。
六、缓存与数据来源
网关只读 Redis 缓存、不连业务库。缓存由 openapi 模块的 ApiRouteCacheServiceImpl 在接口/应用/授权变更时发布,必要时可触发全量刷新。
6.1 业务缓存(openapi 模块发布,网关只读)
| 缓存键 | 结构 | 内容 | 发布方 |
|---|---|---|---|
openapi:route |
Hash | apiId(字符串) → ApiRouteDTO(上游地址、超时、计费方式、价格、免费次数、QPS、状态等) |
ApiRouteCacheServiceImpl#publishRoute |
openapi:route:path |
Hash | METHOD:path → apiId(字符串);METHOD 为 GET/POST/... 或 ALL |
同上 |
openapi:app:credential |
Hash | appKey → AppCredentialDTO(appId、userId、appSecret、IP白名单、QPS/日限额、应用状态、开发者账号状态) |
ApiRouteCacheServiceImpl#doRefreshAppCredential |
openapi:app:auth:{appId} |
Hash | apiId(字符串) → AppAuthDTO(授权状态、剩余免费次数) |
ApiRouteCacheServiceImpl#writeAppAuthCache |
路由匹配顺序:先按请求方法精确匹配 openapi:route:path,未命中再回退 ALL:path;路径先归一化(去掉查询参数、去掉末尾 /)。
接口状态为「维护中(2)」也会进入路径索引,由网关在命中路由后返回 50301;已下线接口不在索引中,表现为 40401。
状态取值(与生产端一致):应用状态 0 正常、开发者账号状态 0 正常、接口状态 2 维护中、授权状态 0 已开通。
6.2 网关自身运行缓存(网关读写的限流/幂等键)
| 缓存键 | 类型 | 用途 | 过期 |
|---|---|---|---|
gateway:nonce:{appKey}:{nonce} |
String(setIfAbsent) | nonce 防重放占用 | gateway.sign.nonce-ttl-seconds |
gateway:qps:{appId}:{apiId}:{qps} |
Redisson 令牌桶(OVERALL) | 应用+接口维度 QPS 限流 | 3600 秒 |
gateway:daily:{appId}:{yyyyMMdd} |
AtomicLong | 应用日调用计数 | 首次自增起 26 小时 |
gateway:idempotent:lock:{requestId} |
String 1(setIfAbsent) |
相同请求ID的处理中锁 | gateway.idempotent.lock-ttl-seconds |
gateway:idempotent:response:{requestId} |
对象 | 首次成功响应,用于幂等回放 | gateway.idempotent.response-ttl-seconds |
七、计费与调用日志
- 转发前预检:限流之后、转发上游之前调用 admin
POST /internal/billing/precheck,同样携带appId、userId、apiId、chargeType、pricePerCall,由 admin 按「免费试用额度 → 次数包 → 钱包余额」只读判断本次调用是否存在可用的付费来源。判定不可支付时网关直接返回40201(msg为具体原因),不调用上游、不产生任何调用成本;免费接口与单价不大于 0 的按次接口不会扣钱包,跳过预检;预检接口不可用时降级继续转发(不再拦截),由扣费阶段兜底:扣费失败返回50002,不回写上游结果。可通过gateway.billing.precheck-enabled=false整体关闭。 - 扣费时机:上游返回 2xx 且响应体业务码为 200 时,在回写调用方响应之前调用 admin
POST /internal/billing/deduct,携带appId、userId、apiId、chargeType、pricePerCall,并以requestId作为幂等键;admin 侧同一requestId不会重复扣费。 - 扣费失败不回写响应:预检通过(或预检被关闭、预检接口降级)后仍可能因并发消耗、admin 内网接口短暂异常导致扣费失败,此时网关丢弃已拿到的上游结果并返回错误响应:admin 明确返回账户不可支付(余额不足、钱包冻结、未开户、仅支持套餐包等)→
40201+ 业务原因;内网计费接口异常/超时 →50002。本次响应不缓存、不返回X-Gateway-Balance,调用日志isCharge=0并写入error_msg;上游非 2xx 或业务码非 200 时不做扣费,按上游结果返回。 - 返回账户余额:上游 2xx 且扣费结果带余额时,网关在响应头回写
X-Gateway-Balance(单位元、4 位小数),数据直接取自扣费接口返回的balanceAfter,不额外查询钱包;扣费失败、上游非 2xx 或未开户时不返回该头。 - 调用日志:请求结束后异步调用
POST /internal/calllog/record(线程池gatewayCallLogExecutor,@Async),记录requestId、appId、userId、apiId、请求/响应时间、耗时、HTTP 状态、上游业务码code、客户端 IP、是否计费、扣费方式/次数/金额、余额、错误信息;缺路由或凭证信息时跳过(admin 侧主键三元组校验要求齐全)。 - 失败也记日志:所有拒绝分支(鉴权、限流、上游失败等)都会写调用日志,
error_msg按api_call_log.error_msg长度(512)截断。 - 幂等回放响应头:命中首次成功响应回放时返回
X-Gateway-Replay: true,并回写首次调用缓存的X-Gateway-Balance余额快照;此时不会再扣费、不会再调用上游。 - 计数时机:QPS/日限额在转发上游之前占用,因此上游失败(502/504)与预检拒绝(40201)也会计入调用量与 QPS。
八、部署与本地启动
8.1 关键参数
| 项 | 值 |
|---|---|
| 进程端口 | 8082(server.port,Docker 中由 SERVER_PORT 覆盖) |
| 应用名 | StarAPI-Gateway |
| 上下文路径 | / |
| 构建产物 | starapi-gateway/target/starapi-gateway.jar |
| profile | 由 Maven profile @profiles.active@ 注入,默认 dev(local 无对应网关配置文件,需显式指定 dev/prod) |
| 内网令牌环境变量 | INTERNAL_API_TOKEN,默认 starapi-internal-token |
8.2 admin 侧配套要求
网关依赖 admin 的内网接口,admin 的 application.yml 必须满足(当前仓库已配置):
| 配置 | 值 | 说明 |
|---|---|---|
security.excludes |
包含 /internal/** |
内网接口放行 Sa-Token 登录校验,改由内网令牌校验 |
internal.api.enabled |
true |
内网接口鉴权开关 |
internal.api.token |
${INTERNAL_API_TOKEN:starapi-internal-token} |
必须与网关 gateway.internal.token 完全一致,否则扣费/日志全部失败 |
internal.api.ip-white-list |
[] |
允许来源 IP,空表示不限制 |
xss.excludeUrls |
包含 /internal/** |
内网 JSON 调用不做 XSS 清洗 |
内网接口:POST /internal/billing/precheck、POST /internal/billing/deduct、POST /internal/calllog/record,请求头 X-Internal-Token。
升级顺序:先升级 admin(提供
/internal/billing/precheck),再升级网关。旧版 admin 上预检会 404,网关按降级策略继续转发并输出 WARN,不会阻断调用。
8.3 本地启动
# 1. 打包(网关依赖 starapi-api 与 common 模块,首次需一起构建)
mvn -pl starapi-gateway -am clean package -Pdev
# 2. 启动(默认 profile=dev,需能连到 dev 环境 Redis)
INTERNAL_API_TOKEN=starapi-internal-token \
java -jar starapi-gateway/target/starapi-gateway.jar --server.port=8082
# 3. 健康检查
curl -s http://127.0.0.1:8082/actuator/health
本地没有 admin 时网关仍可转发上游:转发前预检失败会降级放行,转发成功后扣费失败会返回 50002 错误响应(不回写上游结果),调用日志同样失败(error 日志)。
启动前可用 openapi 管理端触发一次“全量刷新网关缓存”,否则路由/凭证缓存为空,请求会直接返回 40101 / 40401。
8.4 Docker 与 nginx
- 镜像:
starapi/starapi-gateway:6.0.0;Dockerfile构建上下文为starapi-gateway/(ADD ./target/starapi-gateway.jar ./app.jar),工作目录/starapi/gateway,日志写/starapi/gateway/logs(logback./logs),EXPOSE 8082。 - IDEA 已提供
.run/starapi-gateway.run.xml(dockerfile 构建,仅构建镜像)。 script/docker/docker-compose.yml新增starapi-gateway服务:network_mode: "host"、TZ=Asia/Shanghai、SERVER_PORT=8082、日志卷/docker/gateway/logs/:/starapi/gateway/logs/、privileged: true,位于starapi-server2之后。
# 构建镜像(在仓库根目录执行)
docker build -t starapi/starapi-gateway:6.0.0 starapi-gateway
# 只启动/更新网关
docker compose -f script/docker/docker-compose.yml up -d starapi-gateway
script/docker/nginx/conf/nginx.conf已增加upstream gateway { server 127.0.0.1:8082; }与location /api/v1/反代:proxy_pass http://gateway;不带 URI,保留/api/v1前缀;proxy_read_timeout 300s、proxy_buffering off;- location 内不改写、不丢弃
X-App-Key、X-Timestamp、X-Nonce、X-Signature、X-Request-Id,nginx 默认原样透传客户端请求头,签名依赖这些头; - 已设置
Host、X-Real-IP、X-Forwarded-For,网关据此解析真实客户端 IP。
九、已知限制与后续
- 未引入 Spring Cloud Gateway:仓库无 spring-cloud 依赖栈,网关基于 Spring Boot Web + Jetty +
RestClient自行实现转发,保持与既有技术栈一致;上游响应体当前整体读入内存再回写(非流式),大文件下载/SSE 场景需要后续改造。 - 调用日志暂落 MySQL 单表(
api_call_log不分表、不入 Elasticsearch),日志量上来后再做归档/分表/入 ES。 - 限流基于 Redis 令牌桶,按「应用 + 接口 + 有效阈值」维度;键中包含有效 QPS,调整配置后自动切换新桶,多实例共享同一份计数。
- 网关不直连业务库,路由/凭证/授权强依赖 openapi 侧缓存刷新;缓存未刷新或发布失败时只能返回
40101/40401,需在 openapi 管理端重新触发刷新。 - 幂等只缓存 2xx 成功响应,失败响应不缓存;同一
requestId并发时只有一个请求执行,其余返回40901。 - 扣费失败不回写上游结果:上游已执行但扣费失败(如预检后余额被并发消耗、计费接口异常)时,网关丢弃上游结果并返回
40201/50002;调用方重试会再次调用上游,可能产生重复的上游成本。 - 幂等回放在验签与授权之后命中:相同
requestId的重复请求会先完成凭证、IP、签名与接口授权校验,命中首次成功响应后直接回放(返回X-Gateway-Replay: true),不再占用 QPS、不再扣费、不再调用上游;处理中的并发重复请求返回40901。 - QPS/日限额先占后转发,上游失败也计入调用量;如需“仅成功调用计数”,需要把限流挪到转发成功之后并调整令牌桶语义。
- 网关不承载管理页面与后台接口,运营侧能力全部在 admin(openapi / billing / calllog)。