可靠性: PostgreSQL 无 Redis 时 session 不再误用 MySQL 存储
This commit is contained in:
+24
-32
@@ -48,6 +48,29 @@ import (
|
|||||||
"github.com/tamakyi/TamaBox/templates"
|
"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"。
|
// indexNowKeyPattern 限制 IndexNow 密钥文件名的合法形态:32 位小写十六进制 + ".txt"。
|
||||||
// 用于防止把路由参数直接拼进 embed FS 路径时发生路径穿越(如 "../robots.txt")。
|
// 用于防止把路由参数直接拼进 embed FS 路径时发生路径穿越(如 "../robots.txt")。
|
||||||
var indexNowKeyPattern = regexp.MustCompile(`^[0-9a-f]{32}\.txt$`)
|
var indexNowKeyPattern = regexp.MustCompile(`^[0-9a-f]{32}\.txt$`)
|
||||||
@@ -243,38 +266,7 @@ func New() *flamego.Flame {
|
|||||||
gob.Register(time.Time{})
|
gob.Register(time.Time{})
|
||||||
gob.Register(context.Flash{})
|
gob.Register(context.Flash{})
|
||||||
gob.Register(captcha.StoredAnswer{})
|
gob.Register(captcha.StoredAnswer{})
|
||||||
var sessionStorage interface{}
|
sessioner := session.Sessioner(sessionOptions())
|
||||||
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"),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 安全响应头必须注册在静态资源之前:flamego.Static 命中后会终结请求链,
|
// 安全响应头必须注册在静态资源之前:flamego.Static 命中后会终结请求链,
|
||||||
// 放在它后面等于静态资源拿不到 nosniff / HSTS。
|
// 放在它后面等于静态资源拿不到 nosniff / HSTS。
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user