可靠性: PostgreSQL 无 Redis 时 session 不再误用 MySQL 存储

This commit is contained in:
tamakyi
2026-11-05 12:16:46 +08:00
parent 937e688cfa
commit 7636456d4e
2 changed files with 76 additions and 32 deletions
+24 -32
View File
@@ -48,6 +48,29 @@ import (
"github.com/tamakyi/TamaBox/templates"
)
func sessionOptions() session.Options {
options := session.Options{
Initer: session.MemoryIniter(),
Cookie: session.CookieOptions{
HTTPOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: strings.HasPrefix(conf.App.ExternalURL, "https"),
},
}
if conf.Database.DSN != "" && (conf.Database.Type == "" || conf.Database.Type == "mysql") {
options.Initer = mysql.Initer()
options.Config = mysql.Config{DSN: conf.Database.DSN, Lifetime: 7 * 24 * time.Hour}
}
if conf.Redis.Addr != "" {
options.Initer = sessionRedis.Initer()
options.Config = sessionRedis.Config{
Options: &sessionRedis.Options{Addr: conf.Redis.Addr, Password: conf.Redis.Password, DB: 1},
Lifetime: 7 * 24 * time.Hour,
}
}
return options
}
// indexNowKeyPattern 限制 IndexNow 密钥文件名的合法形态:32 位小写十六进制 + ".txt"。
// 用于防止把路由参数直接拼进 embed FS 路径时发生路径穿越(如 "../robots.txt")。
var indexNowKeyPattern = regexp.MustCompile(`^[0-9a-f]{32}\.txt$`)
@@ -243,38 +266,7 @@ func New() *flamego.Flame {
gob.Register(time.Time{})
gob.Register(context.Flash{})
gob.Register(captcha.StoredAnswer{})
var sessionStorage interface{}
initer := session.MemoryIniter()
if conf.Database.DSN != "" {
initer = mysql.Initer()
sessionStorage = mysql.Config{
DSN: conf.Database.DSN,
Lifetime: 7 * 24 * time.Hour,
}
}
if conf.Redis.Addr != "" {
initer = sessionRedis.Initer()
sessionStorage = sessionRedis.Config{
Options: &sessionRedis.Options{
Addr: conf.Redis.Addr,
Password: conf.Redis.Password,
DB: 1,
},
Lifetime: 7 * 24 * time.Hour,
}
}
sessioner := session.Sessioner(session.Options{
Initer: initer,
Config: sessionStorage,
// 显式声明 cookie 安全属性:HTTPOnly 防 XSS 窃取会话;SameSite=Lax 阻断跨站表单
// POST;Secure 仅在 https 对外服务时下发,避免 http 本地开发下 cookie 不生效
// (缺失 Secure 时,HSTS 尚未落地的首次访问可被 MITM 诱导明文请求窃取会话)。
Cookie: session.CookieOptions{
HTTPOnly: true,
SameSite: http.SameSiteLaxMode,
Secure: strings.HasPrefix(conf.App.ExternalURL, "https"),
},
})
sessioner := session.Sessioner(sessionOptions())
// 安全响应头必须注册在静态资源之前:flamego.Static 命中后会终结请求链,
// 放在它后面等于静态资源拿不到 nosniff / HSTS。
+52
View File
@@ -0,0 +1,52 @@
package route
import (
"testing"
"github.com/flamego/session/mysql"
sessionRedis "github.com/flamego/session/redis"
"github.com/stretchr/testify/require"
"github.com/tamakyi/TamaBox/internal/conf"
)
func TestSessionOptionsSelectStorageByDatabaseAndRedis(t *testing.T) {
oldDatabase, oldRedis, oldExternalURL := conf.Database, conf.Redis, conf.App.ExternalURL
t.Cleanup(func() {
conf.Database, conf.Redis, conf.App.ExternalURL = oldDatabase, oldRedis, oldExternalURL
})
conf.Database.DSN = "test-database-dsn"
conf.App.ExternalURL = "https://example.test"
for _, tt := range []struct {
name string
databaseType string
redisAddr string
configType interface{}
}{
{name: "postgres without redis uses memory", databaseType: "postgres"},
{name: "postgres with redis uses redis", databaseType: "postgres", redisAddr: "127.0.0.1:6379", configType: sessionRedis.Config{}},
{name: "mysql without redis uses mysql", databaseType: "mysql", configType: mysql.Config{}},
{name: "mysql with redis uses redis", databaseType: "mysql", redisAddr: "127.0.0.1:6379", configType: sessionRedis.Config{}},
{name: "legacy empty database type uses mysql", configType: mysql.Config{}},
} {
t.Run(tt.name, func(t *testing.T) {
conf.Database.Type = tt.databaseType
conf.Redis.Addr = tt.redisAddr
conf.Redis.Password = "test-only-password"
opts := sessionOptions()
if tt.configType == nil {
require.Nil(t, opts.Config)
return
}
require.IsType(t, tt.configType, opts.Config)
switch cfg := opts.Config.(type) {
case mysql.Config:
require.Equal(t, conf.Database.DSN, cfg.DSN)
case sessionRedis.Config:
require.Equal(t, tt.redisAddr, cfg.Options.Addr)
require.Equal(t, 1, cfg.Options.DB)
}
})
}
}