fix(settings): preserve oauth config compatibility on upgrade

This commit is contained in:
IanShaw027
2026-04-22 12:30:07 +08:00
parent 767f2f2dfe
commit b2e0712190
8 changed files with 830 additions and 99 deletions
+292 -2
View File
@@ -70,6 +70,7 @@ type Config struct {
JWT JWTConfig `mapstructure:"jwt"`
Totp TotpConfig `mapstructure:"totp"`
LinuxDo LinuxDoConnectConfig `mapstructure:"linuxdo_connect"`
WeChat WeChatConnectConfig `mapstructure:"wechat_connect"`
OIDC OIDCConnectConfig `mapstructure:"oidc_connect"`
Default DefaultConfig `mapstructure:"default"`
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
@@ -190,6 +191,25 @@ type LinuxDoConnectConfig struct {
UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
}
type WeChatConnectConfig struct {
Enabled bool `mapstructure:"enabled"`
AppID string `mapstructure:"app_id"`
AppSecret string `mapstructure:"app_secret"`
OpenAppID string `mapstructure:"open_app_id"`
OpenAppSecret string `mapstructure:"open_app_secret"`
MPAppID string `mapstructure:"mp_app_id"`
MPAppSecret string `mapstructure:"mp_app_secret"`
MobileAppID string `mapstructure:"mobile_app_id"`
MobileAppSecret string `mapstructure:"mobile_app_secret"`
OpenEnabled bool `mapstructure:"open_enabled"`
MPEnabled bool `mapstructure:"mp_enabled"`
MobileEnabled bool `mapstructure:"mobile_enabled"`
Mode string `mapstructure:"mode"`
Scopes string `mapstructure:"scopes"`
RedirectURL string `mapstructure:"redirect_url"`
FrontendRedirectURL string `mapstructure:"frontend_redirect_url"`
}
type OIDCConnectConfig struct {
Enabled bool `mapstructure:"enabled"`
ProviderName string `mapstructure:"provider_name"` // 显示名: "Keycloak" 等
@@ -218,6 +238,217 @@ type OIDCConnectConfig struct {
UserInfoUsernamePath string `mapstructure:"userinfo_username_path"`
}
const (
defaultWeChatConnectMode = "open"
defaultWeChatConnectScopes = "snsapi_login"
defaultWeChatConnectFrontendRedirect = "/auth/wechat/callback"
)
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
func normalizeWeChatConnectMode(raw string) string {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "mp":
return "mp"
case "mobile":
return "mobile"
default:
return defaultWeChatConnectMode
}
}
func normalizeWeChatConnectStoredMode(openEnabled, mpEnabled, mobileEnabled bool, mode string) string {
mode = normalizeWeChatConnectMode(mode)
switch mode {
case "open":
if openEnabled {
return "open"
}
case "mp":
if mpEnabled {
return "mp"
}
case "mobile":
if mobileEnabled {
return "mobile"
}
}
switch {
case openEnabled:
return "open"
case mpEnabled:
return "mp"
case mobileEnabled:
return "mobile"
default:
return mode
}
}
func defaultWeChatConnectScopesForMode(mode string) string {
switch normalizeWeChatConnectMode(mode) {
case "mp":
return "snsapi_userinfo"
case "mobile":
return ""
default:
return defaultWeChatConnectScopes
}
}
func normalizeWeChatConnectScopes(raw, mode string) string {
switch normalizeWeChatConnectMode(mode) {
case "mp":
switch strings.TrimSpace(raw) {
case "snsapi_base":
return "snsapi_base"
case "snsapi_userinfo":
return "snsapi_userinfo"
default:
return defaultWeChatConnectScopesForMode(mode)
}
case "mobile":
return ""
default:
return defaultWeChatConnectScopes
}
}
func shouldApplyLegacyWeChatEnv(configKey, envKey string) bool {
if viper.InConfig(configKey) {
return false
}
_, hasNewEnv := os.LookupEnv(envKey)
return !hasNewEnv
}
func applyLegacyWeChatConnectEnvCompatibility(cfg *WeChatConnectConfig) {
if cfg == nil {
return
}
legacyOpenAppID := ""
if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_id", "WECHAT_CONNECT_OPEN_APP_ID") &&
shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
legacyOpenAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_ID"))
if legacyOpenAppID != "" {
cfg.OpenAppID = legacyOpenAppID
}
}
legacyOpenAppSecret := ""
if shouldApplyLegacyWeChatEnv("wechat_connect.open_app_secret", "WECHAT_CONNECT_OPEN_APP_SECRET") &&
shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
legacyOpenAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_OPEN_APP_SECRET"))
if legacyOpenAppSecret != "" {
cfg.OpenAppSecret = legacyOpenAppSecret
}
}
legacyMPAppID := ""
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_id", "WECHAT_CONNECT_MP_APP_ID") &&
shouldApplyLegacyWeChatEnv("wechat_connect.app_id", "WECHAT_CONNECT_APP_ID") {
legacyMPAppID = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_ID"))
if legacyMPAppID != "" {
cfg.MPAppID = legacyMPAppID
}
}
legacyMPAppSecret := ""
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_app_secret", "WECHAT_CONNECT_MP_APP_SECRET") &&
shouldApplyLegacyWeChatEnv("wechat_connect.app_secret", "WECHAT_CONNECT_APP_SECRET") {
legacyMPAppSecret = strings.TrimSpace(os.Getenv("WECHAT_OAUTH_MP_APP_SECRET"))
if legacyMPAppSecret != "" {
cfg.MPAppSecret = legacyMPAppSecret
}
}
if shouldApplyLegacyWeChatEnv("wechat_connect.frontend_redirect_url", "WECHAT_CONNECT_FRONTEND_REDIRECT_URL") {
if legacyFrontend := strings.TrimSpace(os.Getenv("WECHAT_OAUTH_FRONTEND_REDIRECT_URL")); legacyFrontend != "" {
cfg.FrontendRedirectURL = legacyFrontend
}
}
hasLegacyOpen := legacyOpenAppID != "" && legacyOpenAppSecret != ""
hasLegacyMP := legacyMPAppID != "" && legacyMPAppSecret != ""
if shouldApplyLegacyWeChatEnv("wechat_connect.enabled", "WECHAT_CONNECT_ENABLED") && (hasLegacyOpen || hasLegacyMP) {
cfg.Enabled = true
}
if shouldApplyLegacyWeChatEnv("wechat_connect.open_enabled", "WECHAT_CONNECT_OPEN_ENABLED") && hasLegacyOpen {
cfg.OpenEnabled = true
}
if shouldApplyLegacyWeChatEnv("wechat_connect.mp_enabled", "WECHAT_CONNECT_MP_ENABLED") && hasLegacyMP {
cfg.MPEnabled = true
}
if shouldApplyLegacyWeChatEnv("wechat_connect.mode", "WECHAT_CONNECT_MODE") {
switch {
case hasLegacyMP && !hasLegacyOpen:
cfg.Mode = "mp"
case hasLegacyOpen:
cfg.Mode = "open"
}
}
if shouldApplyLegacyWeChatEnv("wechat_connect.scopes", "WECHAT_CONNECT_SCOPES") {
switch {
case hasLegacyMP && !hasLegacyOpen:
cfg.Scopes = defaultWeChatConnectScopesForMode("mp")
case hasLegacyOpen:
cfg.Scopes = defaultWeChatConnectScopesForMode("open")
}
}
}
func normalizeWeChatConnectConfig(cfg *WeChatConnectConfig) {
if cfg == nil {
return
}
cfg.AppID = strings.TrimSpace(cfg.AppID)
cfg.AppSecret = strings.TrimSpace(cfg.AppSecret)
cfg.OpenAppID = strings.TrimSpace(cfg.OpenAppID)
cfg.OpenAppSecret = strings.TrimSpace(cfg.OpenAppSecret)
cfg.MPAppID = strings.TrimSpace(cfg.MPAppID)
cfg.MPAppSecret = strings.TrimSpace(cfg.MPAppSecret)
cfg.MobileAppID = strings.TrimSpace(cfg.MobileAppID)
cfg.MobileAppSecret = strings.TrimSpace(cfg.MobileAppSecret)
cfg.Mode = normalizeWeChatConnectMode(cfg.Mode)
cfg.RedirectURL = strings.TrimSpace(cfg.RedirectURL)
cfg.FrontendRedirectURL = strings.TrimSpace(cfg.FrontendRedirectURL)
cfg.AppID = firstNonEmptyString(cfg.AppID, cfg.OpenAppID, cfg.MPAppID, cfg.MobileAppID)
cfg.AppSecret = firstNonEmptyString(cfg.AppSecret, cfg.OpenAppSecret, cfg.MPAppSecret, cfg.MobileAppSecret)
cfg.OpenAppID = firstNonEmptyString(cfg.OpenAppID, cfg.AppID)
cfg.OpenAppSecret = firstNonEmptyString(cfg.OpenAppSecret, cfg.AppSecret)
cfg.MPAppID = firstNonEmptyString(cfg.MPAppID, cfg.AppID)
cfg.MPAppSecret = firstNonEmptyString(cfg.MPAppSecret, cfg.AppSecret)
cfg.MobileAppID = firstNonEmptyString(cfg.MobileAppID, cfg.AppID)
cfg.MobileAppSecret = firstNonEmptyString(cfg.MobileAppSecret, cfg.AppSecret)
if !cfg.OpenEnabled && !cfg.MPEnabled && !cfg.MobileEnabled && cfg.Enabled {
switch cfg.Mode {
case "mp":
cfg.MPEnabled = true
case "mobile":
cfg.MobileEnabled = true
default:
cfg.OpenEnabled = true
}
}
cfg.Mode = normalizeWeChatConnectStoredMode(cfg.OpenEnabled, cfg.MPEnabled, cfg.MobileEnabled, cfg.Mode)
cfg.Scopes = normalizeWeChatConnectScopes(cfg.Scopes, cfg.Mode)
if cfg.FrontendRedirectURL == "" {
cfg.FrontendRedirectURL = defaultWeChatConnectFrontendRedirect
}
}
// TokenRefreshConfig OAuth token自动刷新配置
type TokenRefreshConfig struct {
// 是否启用自动刷新
@@ -1012,6 +1243,8 @@ func load(allowMissingJWTSecret bool) (*Config, error) {
cfg.LinuxDo.UserInfoEmailPath = strings.TrimSpace(cfg.LinuxDo.UserInfoEmailPath)
cfg.LinuxDo.UserInfoIDPath = strings.TrimSpace(cfg.LinuxDo.UserInfoIDPath)
cfg.LinuxDo.UserInfoUsernamePath = strings.TrimSpace(cfg.LinuxDo.UserInfoUsernamePath)
applyLegacyWeChatConnectEnvCompatibility(&cfg.WeChat)
normalizeWeChatConnectConfig(&cfg.WeChat)
cfg.OIDC.ProviderName = strings.TrimSpace(cfg.OIDC.ProviderName)
cfg.OIDC.ClientID = strings.TrimSpace(cfg.OIDC.ClientID)
cfg.OIDC.ClientSecret = strings.TrimSpace(cfg.OIDC.ClientSecret)
@@ -1207,6 +1440,24 @@ func setDefaults() {
viper.SetDefault("linuxdo_connect.userinfo_id_path", "")
viper.SetDefault("linuxdo_connect.userinfo_username_path", "")
// WeChat Connect OAuth 登录
viper.SetDefault("wechat_connect.enabled", false)
viper.SetDefault("wechat_connect.app_id", "")
viper.SetDefault("wechat_connect.app_secret", "")
viper.SetDefault("wechat_connect.open_app_id", "")
viper.SetDefault("wechat_connect.open_app_secret", "")
viper.SetDefault("wechat_connect.mp_app_id", "")
viper.SetDefault("wechat_connect.mp_app_secret", "")
viper.SetDefault("wechat_connect.mobile_app_id", "")
viper.SetDefault("wechat_connect.mobile_app_secret", "")
viper.SetDefault("wechat_connect.open_enabled", false)
viper.SetDefault("wechat_connect.mp_enabled", false)
viper.SetDefault("wechat_connect.mobile_enabled", false)
viper.SetDefault("wechat_connect.mode", defaultWeChatConnectMode)
viper.SetDefault("wechat_connect.scopes", defaultWeChatConnectScopes)
viper.SetDefault("wechat_connect.redirect_url", "")
viper.SetDefault("wechat_connect.frontend_redirect_url", defaultWeChatConnectFrontendRedirect)
// Generic OIDC OAuth 登录
viper.SetDefault("oidc_connect.enabled", false)
viper.SetDefault("oidc_connect.provider_name", "OIDC")
@@ -1222,8 +1473,8 @@ func setDefaults() {
viper.SetDefault("oidc_connect.redirect_url", "")
viper.SetDefault("oidc_connect.frontend_redirect_url", "/auth/oidc/callback")
viper.SetDefault("oidc_connect.token_auth_method", "client_secret_post")
viper.SetDefault("oidc_connect.use_pkce", false)
viper.SetDefault("oidc_connect.validate_id_token", false)
viper.SetDefault("oidc_connect.use_pkce", true)
viper.SetDefault("oidc_connect.validate_id_token", true)
viper.SetDefault("oidc_connect.allowed_signing_algs", "RS256,ES256,PS256")
viper.SetDefault("oidc_connect.clock_skew_seconds", 120)
viper.SetDefault("oidc_connect.require_email_verified", false)
@@ -1664,6 +1915,45 @@ func (c *Config) Validate() error {
warnIfInsecureURL("linuxdo_connect.redirect_url", c.LinuxDo.RedirectURL)
warnIfInsecureURL("linuxdo_connect.frontend_redirect_url", c.LinuxDo.FrontendRedirectURL)
}
if c.WeChat.Enabled {
weChat := c.WeChat
normalizeWeChatConnectConfig(&weChat)
if weChat.OpenEnabled {
if strings.TrimSpace(weChat.OpenAppID) == "" {
return fmt.Errorf("wechat_connect.open_app_id is required when wechat_connect.open_enabled=true")
}
if strings.TrimSpace(weChat.OpenAppSecret) == "" {
return fmt.Errorf("wechat_connect.open_app_secret is required when wechat_connect.open_enabled=true")
}
}
if weChat.MPEnabled {
if strings.TrimSpace(weChat.MPAppID) == "" {
return fmt.Errorf("wechat_connect.mp_app_id is required when wechat_connect.mp_enabled=true")
}
if strings.TrimSpace(weChat.MPAppSecret) == "" {
return fmt.Errorf("wechat_connect.mp_app_secret is required when wechat_connect.mp_enabled=true")
}
}
if weChat.MobileEnabled {
if strings.TrimSpace(weChat.MobileAppID) == "" {
return fmt.Errorf("wechat_connect.mobile_app_id is required when wechat_connect.mobile_enabled=true")
}
if strings.TrimSpace(weChat.MobileAppSecret) == "" {
return fmt.Errorf("wechat_connect.mobile_app_secret is required when wechat_connect.mobile_enabled=true")
}
}
if v := strings.TrimSpace(weChat.RedirectURL); v != "" {
if err := ValidateAbsoluteHTTPURL(v); err != nil {
return fmt.Errorf("wechat_connect.redirect_url invalid: %w", err)
}
warnIfInsecureURL("wechat_connect.redirect_url", v)
}
if err := ValidateFrontendRedirectURL(weChat.FrontendRedirectURL); err != nil {
return fmt.Errorf("wechat_connect.frontend_redirect_url invalid: %w", err)
}
warnIfInsecureURL("wechat_connect.frontend_redirect_url", weChat.FrontendRedirectURL)
}
if c.OIDC.Enabled {
if strings.TrimSpace(c.OIDC.ClientID) == "" {
return fmt.Errorf("oidc_connect.client_id is required when oidc_connect.enabled=true")