diff --git a/internal/route/route.go b/internal/route/route.go index 94c7d5c..2b0957a 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -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。 diff --git a/internal/route/session_options_test.go b/internal/route/session_options_test.go new file mode 100644 index 0000000..6258bdd --- /dev/null +++ b/internal/route/session_options_test.go @@ -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) + } + }) + } +}