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
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2026 tamakyi. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package shared
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
// RedisPingTimeout 是启动期探活的总超时。
// 刻意短:启动阶段卡住比启动失败更难排查,3 秒足够区分「连不上」与「慢」。
const RedisPingTimeout = 3 * time.Second
// PingRedis 在启动时探一次 Redis 连通性,用于 fail fast。
//
// 为什么必须做这件事:session 建在 Redis 上(route.go 的 sessioner),而
// flamego/session 对 load 错误是直接 panic,被 Recovery 兜成 500 ——
// 也就是 **Redis 一宕,每个请求都 500,包括完全未登录的访客**。
// 等到第一个真实请求才暴露时,运维看到的现场是「站点全 500」,
// 很难第一时间联想到 Redis;启动期直接报错退出,信号干净得多。
//
// 同时显式设置读写超时:go-redis 默认 Dial 5s / Read 3s,Redis 半死不活
// (进程在但不响应)时会让每个请求白等这几秒,表现为整站变慢而无报错。
func PingRedis(addr, password string) error {
ctx, cancel := context.WithTimeout(context.Background(), RedisPingTimeout)
defer cancel()
rdb := redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
// DB 1 与 route.go 里 session 用的库保持一致:探活要探真正会用到的那一个,
// 否则「DB 0 能用、DB 1 被 ACL 禁掉」这种配置错误探不出来。
DB: 1,
DialTimeout: 2 * time.Second,
ReadTimeout: 2 * time.Second,
WriteTimeout: 2 * time.Second,
})
defer func() { _ = rdb.Close() }()
return rdb.Ping(ctx).Err()
}