三处都属于「故障时才发现」的运维缺口,一起改。
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 连接正常」。
68 lines
2.5 KiB
Go
68 lines
2.5 KiB
Go
// 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 route
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestAccessLogNeverRecordsQuery 是本轮修复的回归测试。
|
|
//
|
|
// 修复前用的是 flamego.Classic(),它自带的 Logger() 记录 RequestURI(含 query),
|
|
// 于是 ?t=<私密题钥匙> 与 ?code=<改密凭证> 原样进了日志(本地实测:
|
|
// `path=/_/tblocal/4?t=SECRETTOKEN123`)。拿到这些值可以直接读私密提问或重置密码。
|
|
//
|
|
// 现在要求:不论请求带什么 query,日志里只有 URL.Path。
|
|
func TestAccessLogNeverRecordsQuery(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
target string
|
|
want string
|
|
}{
|
|
{"私密提问 token", "/_/alice/42?t=SECRETTOKEN123", "/_/alice/42"},
|
|
{"改密凭证", "/recover/reset?code=SECRETCODE456", "/recover/reset"},
|
|
{"探索页搜索词", "/explore?q=%E7%A7%98%E5%AF%86&sort=latest", "/explore"},
|
|
{"分页参数", "/explore?page=2", "/explore"},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", c.target, nil)
|
|
fields := accessLogFields(req, 200, 3*time.Millisecond)
|
|
|
|
got, ok := fields["path"].(string)
|
|
require.True(t, ok, "path 字段必须是字符串")
|
|
require.Equal(t, c.want, got)
|
|
require.NotContains(t, got, "?", "path 里不允许出现 query 分隔符")
|
|
|
|
// 兜底:整个字段集合里都不许出现被拦截的凭证
|
|
for k, v := range fields {
|
|
require.NotContains(t, fmt.Sprint(v), "SECRET",
|
|
"字段 %q 泄露了 query 内容:%v", k, v)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAccessLogSkipsStaticAssets 保证静态资源的成功请求不进日志(噪音控制),
|
|
// 但出错时仍然要记 —— 主题图 / 字体 404 正是靠这条排查的。
|
|
func TestAccessLogSkipsStaticAssets(t *testing.T) {
|
|
require.False(t, shouldAccessLog("/static/css/base.css", 200))
|
|
require.False(t, shouldAccessLog("/static/css/base.css", 304))
|
|
require.True(t, shouldAccessLog("/static/css/base.css", 404),
|
|
"静态资源 404 必须记录,否则排查不了图裂")
|
|
require.True(t, shouldAccessLog("/static/css/base.css", 500))
|
|
|
|
// 业务路径一律记录
|
|
require.True(t, shouldAccessLog("/", 200))
|
|
require.True(t, shouldAccessLog("/_/alice", 302))
|
|
require.True(t, shouldAccessLog("/staticish", 200), "只有 /static/ 前缀才算静态资源")
|
|
}
|