网关转发
功能介绍
网关转发是飞鸟快验的一个扩展功能,定位是只做鉴权的透明反向代理:
你的业务系统(比如私有api功能系统,或者任何需要用户身份的独立服务)只监听内网,客户端所有请求先打到飞鸟快验的 /wangGuan/ 路径,快验完成 Token 鉴权后,给请求签发一个 JWT(携带用户身份)再原样转发给你的业务服务。业务服务验一下 JWT 签名就能拿到用户身份,不需要对接任何快验接口,不需要管账号体系,不用写登录。 相比使用公共函数内进行转发请求,使用更简单,性能更高.
三方关系总览
带 Token + WangGuanId
网关不读不改
③ 去掉前缀, 内网转发
怎么看这张图
客户端永远只和网关(公网)打交道;网关和业务服务器之间走内网转发;业务服务器不暴露公网,别人无法绕过网关直连,只能经网关鉴权后访问。
请求流程
/wangGuan/业务路径, Header 加入 Token(快验令牌)、WangGuanId(网关Id)x-wg-error(中文原因) x-wg-jwt / x-wg-ip, 剔除 Token / WangGuanId(防伪造)/wangGuan 前缀, 请求原样转发到业务服务器(内网, 如 127.0.0.1:18899)核心特点:
| 特点 | 说明 |
|---|---|
| 哑管道 | 网关不解析业务报文、不加解密、不记日志、不限流,加解密和协议由客户端与业务服务器自行协商 |
| 零查库转发 | 网关配置与 Token 鉴权全部内存缓存,转发热路径 0 数据库查询 |
| 支持 HTTP 和 WebSocket | WS 握手走同一套鉴权,握手成功后网关只做字节透传,连接归业务服务器 |
| 每网关独立密钥 | 每个网关 Id 一个独立 Secret 签发 JWT,不同业务的 JWT 互不相认,业务之间天然隔离 |
| 防伪造 | x-wg-jwt、x-wg-ip 两个身份头每个请求无条件强制替换,客户端伪造不了 |
WARNING
网关是"新增"功能,与现有 软件用户Api(/Api)、WebSocket(/ws/:Token)、云函数 完全互不影响,旧客户端不需要任何改动。只为自有api快速对接该飞鸟快验用户管理
管理后台:创建网关
管理后台 → 二开扩展 → 网关转发,新增网关:
| 字段 | 说明 |
|---|---|
| 网关名称 | 仅后台管理用,如"短信系统" |
| 业务Url | 业务服务根地址,必须 http:// 或 https:// 开头,如 http://127.0.0.1:18899 |
| 状态 | 启用/禁用。业务出事故时切"禁用"秒级止血,客户端收到明确的 403 而不是超时挂死 |
| 响应超时 | 秒,只限制"等待业务响应头"的阶段,流式响应/大文件不受限(默认60) |
| Secret | JWT 签发密钥。新增时自动生成一个 64 位随机密钥(可直接改),复制保存并配置到业务服务器 |
列表页操作说明:
- 编辑:修改名称/Url/状态/超时/Secret(明文可见,可改可重新随机生成)。修改 Secret 保存后立即生效,业务服务器需同步更新,否则验签失败
- 连通测试:网关侧直接探测业务 Url,显示是否可达、状态码、耗时
- 删除:批量或单个删除,不影响客户端 Token
更换 Secret
网关是单 Secret 设计:重新生成或修改 Secret 立即生效,没有过渡期。更换前请先在业务服务器做好同步准备,网关侧保存后立刻把新 Secret 配置到业务服务器,期间业务会验签失败。
使用场景
只要你有自己的 API 服务(不是快验标准功能),想直接复用快验的用户体系,网关就是为这个准备的:
| 场景 | 说明 |
|---|---|
| 私有 API 系统 | 你写了独立的功能服务(短信业务/查数据/下发指令等),不想自己写注册登录、会员到期、防爆破,业务只管功能本身 |
| 手机端 App 自有接口 | App 已用快验登录,业务接口直接挂在 /wangGuan/ 下,带 Token 就能访问,服务端零账号代码 |
| WebSocket 业务 | 长连接业务(实时消息/推送)同样走网关握手鉴权,连接后协议完全自定 |
| 多个业务服务 | 每个业务建一个网关(独立 Secret),互不影响,都共享同一套快验用户 |
反过来说,如果你用的是快验标准的软件用户Api、卡号充值这些内置功能,不需要网关,直接走 /Api 就行。
客户端怎么请求
请求地址规则:业务路径不变,前面加 /wangGuan。
路径大小写敏感
必须是 wangGuan(W 和 G 大写),写成 /wangguan 会直接 404。
业务原始路径: /api/sms/send
网关地址: https://你的快验域名/wangGuan/api/sms/send
请求时携带两个身份参数,请求头优先,URL 参数兜底(浏览器 WebSocket 不能带自定义头,用 URL 参数):
| 参数 | 位置 | 来源 |
|---|---|---|
Token | 头 Token: xxx 或 参数 ?Token=xxx | 软件用户登录后的令牌,就是调用户Api用的那个 Token(GetToken/UserLogin 返回) |
WangGuanId | 头 WangGuanId: 1 或 参数 ?WangGuanId=1 | 后台「网关转发」列表第一列的 网关Id,告诉网关转发给哪个业务 |
请求体、其他自定义头、Content-Type、query 参数:原样透传,网关一概不读。
网关注入了什么
转发前,网关会对请求头做以下处理:
| 头 | 动作 | 说明 |
|---|---|---|
x-wg-jwt | 强制替换 | 网关签发的 JWT,删除客户端传入的同名头后写入自己的(防伪造) |
x-wg-ip | 强制替换 | 用户真实IP,同样删除客户端传入值后写入(防伪造) |
Token / WangGuanId | 剔除 | 客户端凭证不下传给业务 |
X-Forwarded-For 等代理头 | 剔除 | 防伪造,业务要拿来源IP直接用 x-wg-ip |
Host | 改写 | 改为业务地址的 host |
Location / Content-Location(响应) | 替换 | 业务若返回重定向,内网地址会自动替换回 /wangGuan 前缀,不泄露内网结构 |
| 其余所有头 | 原样透传 |
x-wg-ip 的取值优先级
X-Real-Ip请求头(需要前面 nginx 配置proxy_set_header X-Real-IP $remote_addr;,这个值客户端伪造不了)X-Forwarded-For的最后一跳(由前置代理写入)- TCP 直连地址
JWT 说明
算法 HS256,密钥就是该网关的 Secret(业务服务器配置同一个 Secret 即完成配对)。有效期 60 秒(网关侧同一用户短时间内的请求会复用同一串 JWT,业务侧无需担心重复验签开销)。
Claims 内容:
| 字段 | 类型 | 含义 |
|---|---|---|
uid | int | 快验软件用户 Id(0 表示游客令牌) |
appId | int | 用户所属应用 AppId |
tokenId | int | 在线记录 Id(可用于日志串联) |
user | string | 账号或卡号:账密型应用=登录账号,卡号型应用=卡号 |
key | string | 绑定信息(软件用户表 Key 字段,该应用未注册此用户时为空) |
vipTime | int | 到期时间戳或剩余点数(用户Api GetVipTime 的同款语义) |
vipNumber | float | 积分单独备用字段 |
userClassId | int | 用户分类 Id(0 未分类) |
rmb | float | 余额,仅账密型应用(AppType 1/2)携带,卡号型应用无此字段 |
ip | string | 用户真实IP(与 x-wg-ip 头一致) |
iat / exp | int | 签发/过期时间戳 |
user/key/vipTime/vipNumber/userClassId 来自快验的应用用户表(db_AppUser_{appId})与在线记录,rmb 来自用户表余额,全部随 Token 鉴权一起查询并缓存。业务服务器验签后直接读 claims 即可拿到用户身份/到期/余额,不用再查快验。游客令牌(Uid=0)这些字段是零值。
应用类型说明(后台应用设置里的"软件类型"):1=账号限时, 2=账号计点 为账密型(有账号密码,登录名是账号,携带 rmb);3=卡号限时, 4=卡号计点 为卡号型(只输卡号登录,user 就是卡号,无 rmb 字段)。
完整 JWT 示例
假设某网关的 Secret 为:
6f8a2c19d4e7b35a9c01f8d2e4b6a8c03f5d7b9e1a2c4d6e8f0a1b3c5d7e9f0a
某账密型应用(AppId=10021)的用户 xiaobai01 登录后请求网关,签发出的 JWT 原文(三段用 . 分隔):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBJZCI6MTAwMjEsImV4cCI6MTc5MTIwNDYxOCwiaWF0IjoxNzkxMjA0NTU4LCJpcCI6IjIyMy4xMDIuODguNTkiLCJrZXkiOiJBTUQgUnl6ZW4gNyA2ODAwSCIsInJtYiI6MTI4LjUsInRva2VuSWQiOjE4OTA4OTcsInVpZCI6OCwidXNlciI6InhpYW9iYWkwMSIsInVzZXJDbGFzc0lkIjoyLCJ2aXBOdW1iZXIiOjM1LjUsInZpcFRpbWUiOjE3OTg3NjE2MDB9.XLP0v0oHFYfxcP7WVVREBGEgmbnypcRY0NeDNqq1xVU
第一段 Header(base64 解码后):
{"alg": "HS256", "typ": "JWT"}
第二段 Payload(base64 解码后,就是上面 claims 表的内容):
{
"appId": 10021,
"exp": 1791204618, // 过期时间:签发后60秒
"iat": 1791204558, // 签发时间
"ip": "223.102.88.59",
"key": "AMD Ryzen 7 6800H",
"rmb": 128.5, // 余额,仅账密型应用携带,卡号型应用无此字段
"tokenId": 1890897,
"uid": 8,
"user": "xiaobai01", // 账号;卡号型应用这里就是卡号
"userClassId": 2,
"vipNumber": 35.5,
"vipTime": 1798761600
}
第三段 Signature:对 前两段 用该网关的 Secret 做 HMAC-SHA256(base64 后即上例最后一段)。
把示例 JWT 和上面的 Secret 粘到任意在线解析工具,即可看到解码结果,自己业务里也可以用它验证验签代码写对了没有。
在线解析工具
用于快速查看 JWT 内容、验证签名(均为浏览器本地解码):
- jwt.io 官方 Debugger — 最常用,粘贴 JWT 到右侧 Debugger,Signature 处填
your-256-bit-secret换成网关 Secret,勾选 secret base64 encoded 关闭(快验 Secret 是明文文本密钥,不勾选),下方即可显示校验结果(签名 verified 时勾会变绿) - devtool.tech JWT 解码 — 中文界面
- token.dev — 解码与验签双向
WARNING
在线工具请只在测试阶段使用;这些工具在浏览器本地解码不会上传,但仍建议不要把生产 Secret 和真实用户 Token 长期留在页面里。
业务服务器验签示例(Go + github.com/dgrijalva/jwt-go,gin 中间件;其他 JWT 库同理):
import (
"errors"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
)
func W网关验签() gin.HandlerFunc {
return func(c *gin.Context) {
局_jwt文本 := c.GetHeader("x-wg-jwt")
if 局_jwt文本 == "" {
c.AbortWithStatus(401)
return
}
局_令牌, 局_err := jwt.Parse(局_jwt文本, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { //防算法混淆攻击
return nil, errors.New("签名算法不正确")
}
return []byte("你的网关Secret"), nil //HS256
})
if 局_err != nil || !局_令牌.Valid {
c.AbortWithStatus(401)
return
}
局_声明 := 局_令牌.Claims.(jwt.MapClaims)
c.Set("Uid", int(局_声明["uid"].(float64)))
c.Set("AppId", int(局_声明["appId"].(float64)))
c.Set("Key", 局_声明["key"].(string)) //绑定信息
c.Set("VipTime", int64(局_声明["vipTime"].(float64))) //到期或剩余点数
_ = c.ClientIP() //来源IP直接用 x-wg-ip 头,或 claims["ip"]
}
}
重要
业务服务器必须只监听 127.0.0.1 或内网网卡,并且验签中间件全局强制(包括 WS 握手和静态资源,不留任何例外路由),否则任何人绕过网关直连业务端口就绕过了鉴权。
其他语言(Java/PHP/Python/Node)用任意标准 JWT 库按 HS256 验签即可,无需其他配置。
WebSocket 怎么办
WebSocket 连接的建立本来就是一个 HTTP 请求(GET + Upgrade: websocket),所以:
- 客户端直接对
wss://你的快验域名/wangGuan/ws路径发起 WS 连接,头(或URL参数)带Token和WangGuanId - 网关对握手请求做鉴权并签 JWT,转发给业务
- 业务回
101后,网关把两条连接直接对接做字节透传
之后的心跳、消息格式、在线管理、主动推送全是业务服务器与客户端之间的事,网关一个字节不看。透传期间快验侧注销该 Token 不会断开连接——业务如果需要强制用户重新鉴权,自己定时断开连接即可,客户端重连时会重新经过网关鉴权。
业务服务器拒绝握手(比如验签失败返回 401)时,该响应会原样回给客户端,客户端能立即感知,不会悬挂。
错误怎么判断
鉴权失败、网关禁用、业务不可达等,网关不转发,直接返回:空 body + HTTP 状态码 + 响应头 x-wg-error(中文原因,百分号编码,decodeURIComponent 解码):
| 状态码 | 含义 | 常见原因 |
|---|---|---|
| 401 | token无效 | Token 错误、已注销、已超时,或用了管理员/代理/WebApi 令牌(软件用户与网页用户中心的令牌均可) |
| 403 | 网关已禁用 | 后台把该网关切成了"禁用" |
| 404 | 网关不存在 | WangGuanId 填错,或网关已删除 |
| 502 | 业务服务连接失败 | 业务进程没启动、端口不对、Url 配错 |
| 504 | 业务服务响应超时 | 业务处理太慢,超过网关配置的响应超时 |
只要不是以上状态码,就是业务服务器的正常响应(原样透传,包括业务自己的错误码)。
性能设计
- 转发热路径:0 次数据库查询 + 0 次日志写入 + 0 次 body 解析 + 1 次内存查表 + 1 次内网转发,网关引入的额外延迟在 1ms 以内(内网)
- Token 缓存 30 秒:鉴权查库基本消除。代价是注销一个 Token 后,最多 30 秒内它仍可通过网关(对延迟不敏感的业务完全够用)
- 无效 Token 负缓存 10 秒:防止恶意拿乱猜的 Token 打库
- JWT 复用:同一用户 60 秒内所有请求(含 WS 握手)复用同一串 JWT,签发开销趋近于零
- 连接池:网关到业务服务器常驻内网长连接,每个请求只是复用连接发数据
- WS 透传:每条连接仅 2 个协程,万级连接无压力
部署注意事项
- nginx 放行 WebSocket(前面有 nginx 的话):
proxy_http_version 1.1;+proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";+ 加大proxy_read_timeout,否则 WS 会莫名断开 - nginx 配置
X-Real-IP:proxy_set_header X-Real-IP $remote_addr;,否则x-wg-ip可能拿到的是 nginx 的内网IP - 异常大包/限流交给前置 nginx:网关本身不做任何大小限制和限流(设计如此)
- 网关重启会断开所有透传中的 WS:属预期行为,客户端有重连机制即可,发版选低峰
常见问题
Q: 客户端 Token 是软件用户登录的 Token 吗? 是,就是调用户Api用的那个 Token。网页用户中心(用户H5页)登录得到的令牌同样可以走网关(身份与软件用户一致);管理员后台、代理后台、WebApi 令牌不允许走网关,会返回 401。
Q: Token 长期只走网关,会被快验超时注销吗? 网关不刷新令牌的最后活跃时间(保持零写库)。如果一个纯网关客户端长期不发任何用户Api请求,快验的在线超时机制会注销它,之后请求返回 401,客户端重新登录即可。想避免频繁重登,让客户端隔一段时间调一次快验心跳接口即可。
Q: 一个网关的 Secret 能给另一个网关用吗? 不能,也强烈不建议。每个网关独立 Secret,不同业务的 JWT 互不相认,这是业务之间的隔离边界。
Q: 怎么更换 Secret? 在后台编辑网关,直接修改 Secret(或点"随机生成")保存即生效,没有过渡期。更换时业务会有一小段验签失败窗口,请安排在业务低峰操作,保存后立即把新 Secret 配置到业务服务器。
Q: 业务怎么拿用户到期时间/绑定信息? 验签后直接读 JWT claims 的 vipTime / key / vipNumber / userClassId,这些来自快验的应用用户表,网关已随鉴权一起查出,业务不用再调快验接口。
