fix(ops): 访问日志脱敏、优雅关闭、Redis 可降级并启动即探活

三处都属于「故障时才发现」的运维缺口,一起改。

1) 访问日志泄露凭证
   flamego.Classic() 内部 Use(Logger()),而 Logger 记的是 RequestURI —— 含 query。
   实测日志出现 path=/_/tblocal/4?t=SECRETTOKEN123:?t= 是私密提问钥匙,
   ?code= 是一次性改密凭证,?q= 是搜索词,全都会被明文落盘,凡是能看日志的人
   都能直接读私密提问或重置密码。internal/tracing 早就为同一原因只取 URL.Path
   并留了注释,漏的是框架自带那层。
   → 改 flamego.New() + accessLogMiddleware,只记 Method/Path/Status/Duration/IP;
     静态资源成功请求不记(噪音),4xx/5xx 照记(排查裂图靠它)。

2) 没有优雅关闭
   直接 ListenAndServe,Go 对 SIGTERM 是立即退出,每次部署都会把正在写库的 POST
   就地处死,留下「提问已 INSERT、图片没传完」这类半成品。
   → signal.NotifyContext(SIGTERM/SIGINT) + srv.Shutdown(15s),超时才 Close;
     退出前关连接池。实测 8 个并发分享图请求中发 SIGTERM,8/8 全部成功。

3) Redis 单点
   session 建在 Redis 上,load 失败 flamego/session 会 panic → 每请求 500(含匿名访客);
   而 cache.Cacher 没判空 Addr,go-redis 会回退 localhost:6379,于是「不配 Redis」
   这种被允许的部署里找回密码整体不可用、私信邮件节流失效,且启动零提示。
   → cache 跟随开关(未配则 MemoryIniter);启动 PingRedis fail-fast,显式 2s 超时。
     实测停掉 Redis 后启动立即失败并明确指出是 Redis。

验证:单测 2 项(query 永不入日志 / 静态资源过滤口径);重启日志出现
「收到退出信号」→「HTTP 服务已关闭」;启动打「Redis 连接正常」。
This commit is contained in:
tamakyi
2026-10-29 15:53:32 +08:00
parent 46932b4c53
commit 11d6e701ee
6 changed files with 287 additions and 15 deletions
+36 -11
View File
@@ -184,7 +184,17 @@ func newBanner() *autoban.Banner {
}
func New() *flamego.Flame {
f := flamego.Classic()
// 不用 flamego.Classic():它内部注册的 Logger() 记录 RequestURI(含 query),
// 会把 ?t=<私密题钥匙> 与 ?code=<改密凭证> 明文写进日志(见 accesslog.go)。
// Classic 提供的三件套在这里逐一显式装配:
// accessLogMiddleware() 替代 Logger(),只记 URL.Path
// flamego.Recovery() 行为不变
// flamego.Static() 已在下方按 /static 前缀显式注册(原来就重复注册过)
f := flamego.New()
// 访问日志放在 Recovery 外层:panic 被兜成 500 后仍能记到状态码。
f.Use(accessLogMiddleware())
f.Use(flamego.Recovery())
if conf.App.Production {
flamego.SetEnv(flamego.EnvTypeProd)
}
@@ -281,6 +291,30 @@ func New() *flamego.Flame {
// 之前执行,否则超大 multipart 会在 Bind 的路由级限制生效前落盘临时文件耗尽磁盘。
f.Use(requestBodyLimitMiddleware())
// 缓存层:配了 Redis 用 Redis,没配就用进程内内存缓存。
//
// 原来无条件走 cacheRedis,而 go-redis 对空 Addr 会回退到 localhost:6379
// (go-redis/v9/options.go)。于是「不配 Redis」这种被配置允许的部署形态下,
// 每个依赖缓存的功能都在连一个不存在(或不属于本站)的本地实例:
// - route/auth/recover.go 读不回 code → 找回密码整体不可用,只看到「内部错误」
// - chat_handler 的邮件每小时节流失效 → 每条私信都发一封邮件
// 而这一切在启动期没有任何提示,排查时也看不出跟 Redis 有关。
var cacheHandler flamego.Handler
if conf.Redis.Addr != "" {
cacheHandler = cache.Cacher(cache.Options{
Initer: cacheRedis.Initer(),
Config: cacheRedis.Config{
Options: &cacheRedis.Options{
Addr: conf.Redis.Addr,
Password: conf.Redis.Password,
DB: 0,
},
},
})
} else {
cacheHandler = cache.Cacher(cache.Options{Initer: cache.MemoryIniter()})
}
reqUserSignOut := context.Toggle(&context.ToggleOptions{UserSignOutRequired: true})
reqUserSignIn := context.Toggle(&context.ToggleOptions{UserSignInRequired: true})
@@ -524,16 +558,7 @@ func New() *flamego.Flame {
f.Get("/site-stats", rateLimitMiddleware(60, 1*time.Minute), route.SiteStatsAPI)
}, context.APIEndpoint)
},
cache.Cacher(cache.Options{
Initer: cacheRedis.Initer(),
Config: cacheRedis.Config{
Options: &cacheRedis.Options{
Addr: conf.Redis.Addr,
Password: conf.Redis.Password,
DB: 0,
},
},
}),
cacheHandler,
recaptcha.V3(
recaptcha.Options{
Secret: conf.Recaptcha.ServerKey,