Files
sundynix-pets/pets-be/internal/config/config.go
T
Blizzard 9adafb1354 feat(auth): access token 缩到 2 小时 + refresh token 机制
后台登录框原来预填了 sundynix/sundynix,等于把凭证写在页面上,去掉。

令牌改为 access(2h) + refresh 两段式:
- 新表 sundynix_refresh_tokens,只存 sha256,明文只在签发时返回一次
- 有效期:小程序 30 天、后台 7 天
- 新接口 /api/auth/refresh|logout、/api/admin/refresh|logout

安全约定:
- 每次续期都轮换刷新令牌,旧的立即作废
- 作废后 60 秒内再到达算并发重试放行,超过则判定泄露、吊销该账号全部会话
- 主动退出与被连坐吊销的令牌不吃宽限期,否则「吊销全部」形同虚设
- 禁用用户时一并吊销刷新令牌,最多 2 小时彻底失去访问

两端请求层都做了单飞续期:并发请求同时 401 只发一次 refresh,
否则刷新令牌会被并发轮换掉互相打架。小程序续期失败回退 wx.login,
登录/续期请求标 noAuthRetry,避免登录失败(同样返回 40100)触发自我套娃。

顺带修掉 vite 代理仍指向 8080 的遗留(端口早已改 9090)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-29 09:38:19 +08:00

165 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"errors"
"fmt"
"os"
"strings"
"github.com/spf13/viper"
)
// Config 全局配置
type Config struct {
Server ServerConfig `mapstructure:"server"`
MySQL MySQLConfig `mapstructure:"mysql"`
MinIO MinIOConfig `mapstructure:"minio"`
JWT JWTConfig `mapstructure:"jwt"`
WeChat WeChatConfig `mapstructure:"wechat"`
Auth AuthConfig `mapstructure:"auth"`
Admin AdminConfig `mapstructure:"admin"`
AI AIConfig `mapstructure:"ai"`
}
// AIConfig 大模型配置(provider 中立,openai 兼容 base_url
type AIConfig struct {
Enabled bool `mapstructure:"enabled"`
Provider string `mapstructure:"provider"` // openai(兼容) / mock
BaseURL string `mapstructure:"base_url"` // 如 https://api.deepseek.com
APIKey string `mapstructure:"api_key"` // 用 PETS_AI_API_KEY 覆盖
Model string `mapstructure:"model"` // 如 deepseek-chat
Temperature float64 `mapstructure:"temperature"`
MaxTokens int `mapstructure:"max_tokens"`
TimeoutSec int `mapstructure:"timeout_sec"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type MySQLConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
Charset string `mapstructure:"charset"`
}
// DSN 返回 GORM MySQL 连接串
func (m MySQLConfig) DSN() string {
return fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local",
m.User, m.Password, m.Host, m.Port, m.Database, m.Charset,
)
}
type MinIOConfig struct {
Endpoint string `mapstructure:"endpoint"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
UseSSL bool `mapstructure:"use_ssl"`
PublicBaseURL string `mapstructure:"public_base_url"`
}
// JWTConfig access token 短、refresh token 长:
// access 过期由前端拿 refresh 静默续期,refresh 每次使用都轮换。
type JWTConfig struct {
Secret string `mapstructure:"secret"`
ExpireHours int `mapstructure:"expire_hours"` // access token 有效期
RefreshExpireHours int `mapstructure:"refresh_expire_hours"` // 小程序用户刷新令牌有效期
AdminRefreshExpireHours int `mapstructure:"admin_refresh_expire_hours"` // 后台刷新令牌有效期(更短)
}
type WeChatConfig struct {
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
}
type AuthConfig struct {
DevLogin bool `mapstructure:"dev_login"`
}
type AdminConfig struct {
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
// setDefaults 为每个配置项登记默认值。
// 必须登记:viper 的 AutomaticEnv 只对「已知的 key」生效,未登记的 key 即使设了
// PETS_XXX 环境变量,Unmarshal 时也绑不上(容器里纯 .env 部署会静默取到空值)。
func setDefaults(v *viper.Viper) {
v.SetDefault("server.port", 9090)
v.SetDefault("server.mode", "release")
v.SetDefault("mysql.host", "127.0.0.1")
v.SetDefault("mysql.port", 3306)
v.SetDefault("mysql.user", "root")
v.SetDefault("mysql.password", "")
v.SetDefault("mysql.database", "pets")
v.SetDefault("mysql.charset", "utf8mb4")
v.SetDefault("minio.endpoint", "127.0.0.1:9000")
v.SetDefault("minio.access_key", "")
v.SetDefault("minio.secret_key", "")
v.SetDefault("minio.bucket", "pets")
v.SetDefault("minio.use_ssl", false)
v.SetDefault("minio.public_base_url", "")
v.SetDefault("jwt.secret", "")
v.SetDefault("jwt.expire_hours", 2)
v.SetDefault("jwt.refresh_expire_hours", 720) // 30 天
v.SetDefault("jwt.admin_refresh_expire_hours", 168)
v.SetDefault("wechat.app_id", "")
v.SetDefault("wechat.app_secret", "")
v.SetDefault("auth.dev_login", false)
v.SetDefault("admin.username", "sundynix")
v.SetDefault("admin.password", "")
v.SetDefault("ai.enabled", false)
v.SetDefault("ai.provider", "openai")
v.SetDefault("ai.base_url", "")
v.SetDefault("ai.api_key", "")
v.SetDefault("ai.model", "")
v.SetDefault("ai.temperature", 0.6)
v.SetDefault("ai.max_tokens", 1024)
v.SetDefault("ai.timeout_sec", 30)
}
// Load 载入配置:默认值 → configs/config.yaml(可选)→ 环境变量覆盖。
// 环境变量用 PETS_ 前缀、点转下划线,例如 PETS_MYSQL_PASSWORD 覆盖 mysql.password。
// 容器部署不带 config.yaml,全部走 .env 注入的环境变量。
func Load() (*Config, error) {
v := viper.New()
setDefaults(v)
v.SetConfigName("config")
v.SetConfigType("yaml")
v.AddConfigPath("./configs")
v.AddConfigPath("../../configs")
v.AddConfigPath(".")
v.SetEnvPrefix("PETS")
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// 配置文件可选:找不到就只用默认值 + 环境变量;文件存在但解析失败仍要报错。
if err := v.ReadInConfig(); err != nil {
var notFound viper.ConfigFileNotFoundError
if !errors.As(err, &notFound) && !os.IsNotExist(err) {
return nil, fmt.Errorf("read config: %w", err)
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unmarshal config: %w", err)
}
return &cfg, nil
}