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
@@ -0,0 +1,56 @@
package middleware
import (
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/auth"
)
// A5:进程内兜底限流——固定窗口内超限即拒,过窗后恢复。Redis 挂时靠它 fail-safe。
func TestProcLimiter(t *testing.T) {
l := newProcLimiter(time.Minute)
now := time.Now()
for i := 0; i < 3; i++ {
if !l.allow("k", 3, now) {
t.Fatalf("前 3 次应放行(第 %d 次被拒)", i+1)
}
}
if l.allow("k", 3, now) {
t.Fatal("超限第 4 次应拒")
}
if !l.allow("k", 3, now.Add(2*time.Minute)) {
t.Fatal("新窗口应恢复放行")
}
}
// A6AuthFromHeaderOrQuery——?token= 有效则注入 uid、无 token 则 401。
func TestAuthFromHeaderOrQuery(t *testing.T) {
gin.SetMode(gin.TestMode)
tok, err := auth.Issue("u1")
if err != nil {
t.Fatalf("签发失败: %v", err)
}
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/x?token="+tok, nil)
AuthFromHeaderOrQuery()(c)
if c.IsAborted() {
t.Fatal("有效 query token 不该被拦")
}
if v, _ := c.Get(CtxUserID); v != "u1" {
t.Fatalf("应注入 uid=u1,得 %v", v)
}
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
c2.Request = httptest.NewRequest("GET", "/x", nil)
AuthFromHeaderOrQuery()(c2)
if !c2.IsAborted() || w2.Code != 401 {
t.Fatalf("无 token 应 401,得 aborted=%v code=%d", c2.IsAborted(), w2.Code)
}
}