fix(prod): 后端生产级 A 类硬伤全清(7 项:授权/崩溃点/全表扫/限流/鉴权/限额)

部署前生产级审计(可靠性/数据层/安全三路)后,清掉 7 处代码级硬伤:

A1 后台定时器 goroutine 无 panic recover → 单个 DB panic 崩整个 gateway。加 safeGo/
   safeCall,包住订阅/掉单补偿/微信推送/探针 goroutine,单轮 tick 再兜一层。
A2 提示词控制面(建/激活/停用,热广播全服务)只 RequireAuth → 任意登录用户改全局提示词。
   三写端点+列表挂 RequireAdmin。
A3 HITL 审批端点无角色门 → viewer 可放行烧钱执行。加 RequireTenantRole(member)。
A4 审计/护栏列表 limit 无校验,limit=-1 让 gorm 取消 LIMIT 全表扫。加 clampLimit/
   clampOffset,AdminTasks/AdminSpaces 补上界。
A5 限流 Redis 一挂就完全放行(fail-open)。加进程内固定窗口兜底(fail-safe) + 登录/注册
   按 IP 专用严限流(10/min)。
A6 公开 by-id 端点(stream/exec/report导出/kb导入流)无鉴权无租户过滤。加
   AuthFromHeaderOrQuery(从 ?token= 取 JWT) + task/report 按 owner 归属校验;桌面端
   5 处 EventSource/下载 URL 经 tokenQuery 附 JWT。
A7 文件上传无大小上限(整文件进内存 OOM 面) → 50MB 闸(KB_MAX_UPLOAD_BYTES)+ LimitReader;
   http.Server 加 ReadHeaderTimeout/ReadTimeout/MaxHeaderBytes(不设 WriteTimeout 保 SSE)。

带单测:clampLimit/safeCall/procLimiter/AuthFromHeaderOrQuery/TaskOwner。
build+vet+全量 test 绿;desktop tsc 绿。B(迁移工具/实时探针/出网韧性/登录锁定/leader选举)
与 C(TLS/PG HA/K8s/备份自动化/可观测)分期后做,参照 production_readiness.md。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 15:04:47 +08:00
parent ce7cca657e
commit ac38d5e663
20 changed files with 443 additions and 100 deletions
+18 -13
View File
@@ -59,15 +59,18 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
// 微信扫码登录(全公开:ticket 是唯一凭证;PC 建票与轮询都无鉴权头)
api.POST("/wx/mp/ticket", h.WxMPTicket) // PC 建票 + 微信二维码
api.GET("/wx/mp/poll", h.WxMPPoll) // PC 轮询登录态
api.POST("/auth/register", h.Register) // 注册 + 签发 JWT
api.POST("/auth/login", h.Login) // 登录 + 签发 JWT
// 登录/注册爆破面大:在全局限流之上再叠一道更严的按 IP 专用限流(10/min)。
api.POST("/auth/register", middleware.RateLimitN(cache, 10, "auth-reg"), h.Register) // 注册 + 签发 JWT
api.POST("/auth/login", middleware.RateLimitN(cache, 10, "auth-login"), h.Login) // 登录 + 签发 JWT
api.GET("/auth/me", h.Me) // 当前登录用户(无效令牌 → 401)
api.GET("/health", h.Health) // 依赖健康聚合(顶栏五盏灯)
api.GET("/tasks/:id/stream", h.StreamTask) // SSE 回流 Token Streamtask_id 寻址)
api.GET("/tasks/:id/exec", h.StreamExec) // SSE 回流执行轨迹(task_id 寻址)
api.GET("/kb/ingest/:id/stream", h.KbIngestStream) // 入库进度 SSEjob_id 寻址)
api.GET("/reports/:id/export", h.ExportReport) // 按需导出(report_id 寻址)
api.GET("/reports/:id/download", h.ExportReport) // 兼容旧入口(默认 docx
// 这些 by-id 端点带不了 Bearer 头(EventSource/下载),改由 AuthFromHeaderOrQuery
// 从 ?token= 取 JWT 强制鉴权;task/report 三个再按 owner 归属校验(handler 内)。
api.GET("/tasks/:id/stream", middleware.AuthFromHeaderOrQuery(), h.StreamTask) // SSE 回流 Token Stream
api.GET("/tasks/:id/exec", middleware.AuthFromHeaderOrQuery(), h.StreamExec) // SSE 回流执行轨迹
api.GET("/kb/ingest/:id/stream", middleware.AuthFromHeaderOrQuery(), h.KbIngestStream) // 入库进度 SSE(登录即可,进度非敏感
api.GET("/reports/:id/export", middleware.AuthFromHeaderOrQuery(), h.ExportReport) // 按需导出
api.GET("/reports/:id/download", middleware.AuthFromHeaderOrQuery(), h.ExportReport) // 兼容旧入口(默认 docx
api.POST("/billing/callback/:channel", h.PaymentCallback) // 支付回调(渠道服务器带不了 Bearer;渠道验签是唯一的门)
// —— 受保护:owner 作用域业务,必须携带有效 JWT ——
@@ -75,7 +78,7 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
{
p.POST("/tasks", middleware.RequireTenantRole(db, store.RoleMember), h.SubmitTask) // 提交任务(烧租户积分):viewer 只读拦下
p.GET("/tasks/:id", h.TaskStatus) // 任务生命周期状态(UI 轮询 submitted/running/done/failed/timeout/waiting/rejected
p.POST("/tasks/:id/approve", middleware.Audit(db), h.ApproveTask) // HITL 人工审批决定(批准/拒绝,审计
p.POST("/tasks/:id/approve", middleware.RequireTenantRole(db, store.RoleMember), middleware.Audit(db), h.ApproveTask) // HITL 人工审批:≥member(放行会烧租户积分,viewer 拦下),审计
p.GET("/tenants/current", h.TenantCurrent) // 当前租户上下文 + 角色 + 可花余额(多租户)
p.GET("/me/tenants", h.MyTenantsList) // 我所属租户(供切换)
p.POST("/me/tenants", h.CreateMyTenant) // 自助建组织(创建者即 owner,建完切入)
@@ -104,11 +107,13 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
p.GET("/kb/doc", h.KbDoc) // 取单篇文档
p.DELETE("/kb/doc", middleware.RequireSpaceRole(db, store.RoleMember), h.KbDeleteDoc) // 级联删文档:viewer 拦下
// Prompt 控制面(平台级配置:建版本 → 激活 → 控制面热下发各服务)
p.GET("/prompts", h.PromptList) // 列出全部版本 + 可配键
p.POST("/prompts/version", h.PromptCreateVersion) // 建新版本(不自动激活)
p.POST("/prompts/activate", middleware.Audit(db), h.PromptActivate) // 激活某版本 → 广播热更新(审计)
p.POST("/prompts/deactivate", middleware.Audit(db), h.PromptDeactivate) // 撤销激活 → 回退代码默认(热,审计
// Prompt 控制面(平台级配置:建版本 → 激活 → 控制面热下发各服务)
// 全平台配置、activate 热广播到所有服务、影响所有租户输出 —— 必须 RequireAdmin
// 否则任意登录用户可改全局提示词。读写同源都挂 admin。
p.GET("/prompts", middleware.RequireAdmin(), h.PromptList) // 列出全部版本 + 可配键
p.POST("/prompts/version", middleware.RequireAdmin(), middleware.Audit(db), h.PromptCreateVersion) // 建新版本(不自动激活
p.POST("/prompts/activate", middleware.RequireAdmin(), middleware.Audit(db), h.PromptActivate) // 激活某版本 → 广播热更新(审计)
p.POST("/prompts/deactivate", middleware.RequireAdmin(), middleware.Audit(db), h.PromptDeactivate) // 撤销激活 → 回退代码默认(热,审计)
p.GET("/kb/links", h.KbLinks) // 某库双链
p.POST("/kb/note", middleware.RequireSpaceRole(db, store.RoleMember), h.KbSaveNote) // 新建/编辑笔记:viewer 拦下
p.GET("/kb/graph", h.KbGraph) // 知识图谱三元组