Weave Router 反馈链接设计:无登录反馈页 + HMAC 签名 token 如何保证安全(完整指南)
【免费下载链接】routerModel router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.项目地址: https://gitcode.com/GitHub_Trending/router101/router
Weave Router 是一个面向智能体系统的模型路由器(Model Router),能在 50ms 内把每个提示词路由到最合适的模型,仅通过更换一个端点即可降低 40%~70% 的成本。它的响应末尾会附带一条无登录反馈链接(no-login feedback link),用户无需注册或登录就能对路由决策点赞/点踩。那么这条公开链接凭什么安全?答案是一枚 HMAC-SHA256 签名 token——签名密钥只留在服务端,链接本身即唯一凭证。
为什么反馈链接不需要用户登录?
传统做法是让反馈页挂登录态:但终端里的 Claude Code、Codex 用户根本没有"登录 Weave 后台"的场景,要求登录后才能点一下"👍",反馈率会直接崩盘。
Weave Router 的思路相反:
- 链接即凭证:URL 里的那串 token 就是唯一的身份凭证,公开路由不挂任何鉴权中间件
- 一次一链:token 在路由完成的那一刻为"这一个请求"单独签发,不可猜测、不可复用
- 泄露无爆炸半径:最坏情况也就是别人能给你这一条请求评个分,碰不到其他请求、更碰不到其他安装
这套设计落在 internal/feedback/token.go 中,代码注释写得很直白:"纯函数(HMAC + base64,无 I/O),Weave 后端永远看不到密钥"。
一枚签名 token 里装了什么?
签发逻辑在 internal/feedback/token.go 的Signer.Mint中。token 结构只有一段:
base64url(JSON载荷) + "." + base64url(HMAC-SHA256签名)载荷(Claims结构体)包含 5 个字段:
| 字段 | 含义 |
|---|---|
iid | 安装 ID(定位路由上下文) |
eid | 外部 ID(把反馈归属到 Weave 组织) |
rid | 请求 ID(反馈针对哪一次路由) |
uid | 可选,路由器用户 ID(归属到具体用户) |
exp | Unix 过期时间戳,0 表示永不过期 |
关键点:载荷是明文、签名是密文。服务端不存任何会话表,验证时只需用自己的密钥对同样的载荷重新算一遍 HMAC 并比对——这就是"无状态"的含义:任何一台持有密钥的实例都能验证任何 token,天然横向扩展。
三个公开端点:路由决策的反馈闭环
服务端的挂载点在 internal/server/server.go,只有配置了ROUTER_FEEDBACK_LINK_SECRET后才会挂载/v1/feedback分组。处理逻辑集中在 internal/api/feedback/feedback.go:
| 端点 | 作用 |
|---|---|
GET /v1/feedback/link/:token | 打开无登录反馈页,返回该请求的路由上下文(选了哪个模型/供应商)和已有评价 |
POST /v1/feedback/link | 提交评分 + 可选文字评论(请求体上限 64KB,评分只能是 up/down) |
GET /v1/feedback/rate?t=<token>&r=up|down | 终端响应尾部内嵌的一键点赞/点踩链接,直接返回一个小小的确认页 |
其中x-router-feedback-url响应头由 internal/proxy/feedback.go 中的setFeedbackLinkHeader写入——即每次推理响应发出前,路由器顺手把base/f/<token>塞进响应头,GUI 客户端据此渲染反馈入口。
状态码的讲究:404 vs 410
一个容易忽略但很体现功力的细节:token 校验失败会映射成两种不同的 HTTP 状态码(见writeTokenError):
- 404 Not Found—— token 格式错误或签名不符,链接"根本不存在"
- 410 Gone—— 签名有效但已过期,链接"曾经存在但现在作废"
这样前端页面和用户能区分"你拿到的是坏链接"和"链接过期了",而不是笼统地报 500。
HMAC-SHA256 验证链路:安全细节在哪里?
验证函数Verify(internal/feedback/token.go)按四步走,每一步都在堵一种攻击面:
- 切分与格式检查—— 按第一个
.切出载荷和签名,缺任一半直接判无效 - 恒定时间比较—— 用
crypto/subtle.ConstantTimeCompare比对签名,防止攻击者通过响应耗时差异逐字节"猜"出签名(时序侧信道) - 解码并反序列化载荷—— base64 解码失败或 JSON 解析失败一律判无效
- 过期判断—— 签名通过但
exp已到时,返回专门的"已过期"错误(对应 410)
签发端同样有防御性设计:Mint在序列化这类固定结构时理论上不会失败,但代码仍选择"降级为不输出链接"而不是在请求路径上 panic;未配置密钥时NewSigner返回 nil,Mint返回空串——对调用方来说"功能未启用"和"token 无效"不可区分,不泄露部署配置。
配置开关:两个环境变量决定功能是否启用
组合根在 cmd/router/main.go 中接线:
ROUTER_FEEDBACK_LINK_SECRET—— HMAC 签名密钥。不设置则整个功能关闭,响应不带反馈链接头ROUTER_FEEDBACK_BASE_URL—— 反馈页的公开基础 URL;只设密钥没设它,端点会挂载但不会对外暴露链接- token TTL 也可配置,非正数 TTL 表示"永不过期"
这种"缺配置即降级"的模式让自托管用户零负担:不需要反馈功能就什么都不用做。
数据落地:反馈存到哪里、如何回流?
提交路径在 internal/proxy/feedback.go 的SubmitFeedback:
- 先写数据库(权威数据源):upsert 进路由器自己的反馈表,表结构见迁移文件 db/migrations/0020_router-feedback.up.sql
- 再发遥测 span(尽力而为):向 Weave 后端发一个
router.feedbackOTLP span,由后端镜像到其自身的反馈表;span 队列满了就丢弃,不影响用户提交
也就是说:数据库写入是权威,遥测只是同步通道——顺序反过来的话,监控系统抖动就会丢真实反馈。
彩蛋:终端里的 /rf 快捷指令
对 Claude Code、Codex 这类终端客户端,链接头之外还有一层更"土味"但更顺手的交互:流式响应结束后会追加一行尾部提示,教用户输入/rf +(好体验)或/rf -(差体验,可附注,如/rf - too slow)。相关命令说明见 install/commands/rf.md。
两个耐人寻味的设计决策:
- 尾提示刻意不带链接:Claude Code 的 markdown 渲染会把
label打印成label (url),内嵌签名链接等于把整串 token 抖在屏幕上,反而泄露凭证 - 只在"真聊天"回合出现:IDE 内联编辑、diff 应用、commit message 等表面会被排除;子代理派发、压缩、标题生成等机器回合也不会追加——尾提示只会出现在用户直接发起的主循环/工具结果回合
安全设计小结
| 机制 | 防住的攻击 |
|---|---|
| 每次请求独立签发 token | 一个链接不能评价/篡改其他请求 |
| HMAC-SHA256 签名 + 恒定时间比较 | 伪造签名、时序侧信道逐字节爆破 |
| TTL 过期 + 404/410 语义分离 | 链接长期滞留、钓鱼复用 |
| 请求体 64KB 上限、评分白名单(up/down) | 恶意客户端刷超大请求体 |
| 密钥不出服务端、载荷无敏感信息 | 即使链接外泄也无从推导密钥或他人身份 |
| 缺配置即静默关闭 | 误开公开端点的运维风险 |
一句话总结:Weave Router 用"一次性、可自验证、带过期"的签名 token 替代了登录态,让一个零门槛的反馈按钮在不牺牲任何安全性的前提下,嵌进了每一次模型路由的闭环里。想深入源码,从 internal/feedback/token.go(签发与验证)和 internal/api/feedback/feedback.go(HTTP 层)这两个文件读起即可。
【免费下载链接】routerModel router for agentic systems. Routes every prompt to the right model in <50ms. Cut costs 40-70% with just an endpoint change.项目地址: https://gitcode.com/GitHub_Trending/router101/router
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考