e41085a878
会话安全(jti 轮换 / 重用检测 / 改密吊销 / 禁用即时下线 / 清理 / 失败登录落库): - refresh token 轮换 jti + token-family 重用检测,旧 token 重放即吊销整条会话 - 改密码、停用用户即时吊销其全部活跃会话(revoked_by 审计) - 中间件 session JOIN user 校验,禁用/删除用户带 token 请求返回 401 USER_DISABLED - 新增 login_attempts 失败登录落库 + 会话保留期清理 goroutine 授权实时 phase + 心跳回带: - LicenseGuard 改为按当前 DB 实时计算 phase(30s 每店缓存),续费/过期/被改 ~30s 内对写操作生效,无需重登 - /auth/ping 回带授权概况(ShopInfoView,与 /license/info 同构),客户端一次心跳即刷新横幅/门禁 首次使用自动试用 + code-review 修复: - 门店首次登录/续期无有效授权时自动签发 30 天 trial(快路径无锁 Count,仅首用走 FOR UPDATE 事务) - ShopInfo 区分「确无授权」与瞬时 DB 错误,避免误降级 - trial 签发后改为在事务提交后再失效 phase 缓存(修复早于提交的竞态) - 存量无 sid token 续期纳入显式上限,legacy 会话不再游离于并发配额之外 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
110 lines
4.3 KiB
Go
110 lines
4.3 KiB
Go
package config
|
|
|
|
import (
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type Config struct {
|
|
Server ServerConfig
|
|
Database DatabaseConfig
|
|
JWT JWTConfig
|
|
License LicenseConfig
|
|
Storage StorageConfig
|
|
Session SessionConfig
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Port string `mapstructure:"port"`
|
|
Mode string `mapstructure:"mode"` // debug | release
|
|
CORSOrigin string `mapstructure:"cors_origin"` // 允许的 CORS 来源,生产设为具体域名
|
|
}
|
|
|
|
type DatabaseConfig struct {
|
|
DSN string `mapstructure:"dsn"`
|
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
|
}
|
|
|
|
type JWTConfig struct {
|
|
Secret string `mapstructure:"secret"`
|
|
AccessExpireMin int `mapstructure:"access_expire_min"` // Access Token 有效分钟数
|
|
RefreshExpireH int `mapstructure:"refresh_expire_h"` // Refresh Token 有效小时数
|
|
}
|
|
|
|
type LicenseConfig struct {
|
|
HMACSecret string `mapstructure:"hmac_secret"` // legacy, kept for backward compat
|
|
Ed25519PublicKey string `mapstructure:"ed25519_public_key"` // base64 Ed25519 public key for token verification
|
|
Ed25519PrivateKey string `mapstructure:"ed25519_private_key"` // base64 Ed25519 private key for token signing (keep in Bitwarden)
|
|
}
|
|
|
|
// SessionConfig 登录会话与并发限制(全局默认,可被每店 session_policy 覆盖)。
|
|
type SessionConfig struct {
|
|
LimitDesktop int `mapstructure:"limit_desktop"` // 桌面端(win/mac/linux)最大并发会话,0=禁止
|
|
LimitMobile int `mapstructure:"limit_mobile"` // 移动端(android/ios)最大并发会话,0=禁止
|
|
LimitWeb int `mapstructure:"limit_web"` // web 端最大并发会话,0=禁止
|
|
MaxFailures int `mapstructure:"max_failures"` // 连续登录失败几次后锁定
|
|
LockMinutes int `mapstructure:"lock_minutes"` // 锁定时长(分钟)
|
|
RetentionDays int `mapstructure:"retention_days"` // 已撤销/过期会话与失败登录记录的保留天数,过期后台清理
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
UploadDir string `mapstructure:"upload_dir"`
|
|
BaseURL string `mapstructure:"base_url"`
|
|
PublicURL string `mapstructure:"public_url"` // 商品公开页基础 URL,用于生成二维码
|
|
WebDir string `mapstructure:"web_dir"` // Flutter web 构建产物目录,用于 OG 标签注入
|
|
}
|
|
|
|
var C Config
|
|
|
|
func Load() {
|
|
viper.SetConfigName("config")
|
|
viper.SetConfigType("yaml")
|
|
viper.AddConfigPath(".")
|
|
viper.AddConfigPath("./config")
|
|
|
|
// 环境变量覆盖(生产部署时使用)
|
|
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
viper.AutomaticEnv()
|
|
|
|
// 显式绑定没有默认值的 key,确保 AutomaticEnv 能找到对应 env var
|
|
_ = viper.BindEnv("database.dsn", "DATABASE_DSN")
|
|
_ = viper.BindEnv("jwt.secret", "JWT_SECRET")
|
|
_ = viper.BindEnv("license.hmac_secret", "LICENSE_HMAC_SECRET")
|
|
_ = viper.BindEnv("license.ed25519_public_key", "LICENSE_ED25519_PUBLIC_KEY")
|
|
_ = viper.BindEnv("license.ed25519_private_key", "LICENSE_ED25519_PRIVATE_KEY")
|
|
_ = viper.BindEnv("storage.upload_dir", "STORAGE_UPLOAD_DIR")
|
|
_ = viper.BindEnv("storage.base_url", "STORAGE_BASE_URL")
|
|
_ = viper.BindEnv("storage.public_url", "STORAGE_PUBLIC_URL")
|
|
_ = viper.BindEnv("storage.web_dir", "STORAGE_WEB_DIR")
|
|
|
|
// 默认值
|
|
viper.SetDefault("server.port", "8080")
|
|
viper.SetDefault("server.mode", "debug")
|
|
viper.SetDefault("server.cors_origin", "*")
|
|
viper.SetDefault("jwt.access_expire_min", 60)
|
|
viper.SetDefault("jwt.refresh_expire_h", 168) // 7天
|
|
viper.SetDefault("session.limit_desktop", 2)
|
|
viper.SetDefault("session.limit_mobile", 2)
|
|
viper.SetDefault("session.limit_web", 2) // 默认不禁 web(官网挂着 Web 版 app);设 0 可禁
|
|
viper.SetDefault("session.max_failures", 5)
|
|
viper.SetDefault("session.lock_minutes", 15)
|
|
viper.SetDefault("session.retention_days", 90)
|
|
viper.SetDefault("database.max_idle_conns", 10)
|
|
viper.SetDefault("database.max_open_conns", 100)
|
|
viper.SetDefault("storage.upload_dir", "./uploads/images")
|
|
viper.SetDefault("storage.base_url", "http://localhost:8080/images")
|
|
viper.SetDefault("storage.public_url", "http://localhost:8081")
|
|
viper.SetDefault("storage.web_dir", "./web")
|
|
|
|
if err := viper.ReadInConfig(); err != nil {
|
|
log.Println("[config] no config file found, using defaults and env vars")
|
|
}
|
|
|
|
if err := viper.Unmarshal(&C); err != nil {
|
|
log.Fatalf("[config] failed to unmarshal config: %v", err)
|
|
}
|
|
}
|