@@ -3,9 +3,12 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 安全设置键:WAF 参数 / 真实IP请求头 / 面板地址(app_url)。
|
||||
@@ -147,17 +150,49 @@ func (p SecurityPatch) merge(dst *SecuritySettings) map[string]bool {
|
||||
return touched
|
||||
}
|
||||
|
||||
// UpdateSecurity 把补丁合并到现值上整体校验,只落库出现的字段,再重读刷新快照。
|
||||
type securityUpdate struct {
|
||||
next SecuritySettings
|
||||
touched map[string]bool
|
||||
values map[string]string
|
||||
}
|
||||
|
||||
// UpdateSecurity 供启动与内部配置使用;HTTP PATCH 使用带令牌证明的变体。
|
||||
func (s *SettingService) UpdateSecurity(ctx context.Context, p SecurityPatch) error {
|
||||
cur, err := s.Security(ctx)
|
||||
return s.updateSecurity(ctx, p, nil)
|
||||
}
|
||||
|
||||
// UpdateSecurityAuthenticated 在写事务持锁后复核请求令牌,防撤销后的慢请求落库。
|
||||
func (s *SettingService) UpdateSecurityAuthenticated(
|
||||
ctx context.Context, p SecurityPatch, auth *AuthService, username string, proof TokenProof,
|
||||
) error {
|
||||
check := &authenticatedMutation{auth: auth, username: username, proof: proof}
|
||||
return s.updateSecurity(ctx, p, check)
|
||||
}
|
||||
|
||||
func (s *SettingService) updateSecurity(ctx context.Context, p SecurityPatch, check *authenticatedMutation) error {
|
||||
up, err := s.prepareSecurityUpdate(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
touched := p.merge(&cur)
|
||||
if err := validateSecurity(&cur); err != nil {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
return s.applySecurityUpdateTx(tx, up, check)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
kv := map[string]string{
|
||||
return s.ReloadSecurity(context.WithoutCancel(ctx))
|
||||
}
|
||||
|
||||
func (s *SettingService) prepareSecurityUpdate(ctx context.Context, p SecurityPatch) (securityUpdate, error) {
|
||||
cur, err := s.Security(ctx)
|
||||
if err != nil {
|
||||
return securityUpdate{}, err
|
||||
}
|
||||
touched := p.merge(&cur)
|
||||
if err := validateSecurity(&cur); err != nil {
|
||||
return securityUpdate{}, err
|
||||
}
|
||||
values := map[string]string{
|
||||
settingSecLoginFailLimit: strconv.Itoa(cur.LoginFailLimit),
|
||||
settingSecLoginLockMin: strconv.Itoa(cur.LoginLockMinutes),
|
||||
settingSecIPRateRPS: strconv.Itoa(cur.IPRateRPS),
|
||||
@@ -165,13 +200,100 @@ func (s *SettingService) UpdateSecurity(ctx context.Context, p SecurityPatch) er
|
||||
settingSecRealIPHeader: cur.RealIPHeader,
|
||||
settingSecAppURL: cur.AppURL,
|
||||
}
|
||||
for key := range touched {
|
||||
if err := s.set(ctx, key, kv[key]); err != nil {
|
||||
return securityUpdate{next: cur, touched: touched, values: values}, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) applySecurityUpdateTx(tx *gorm.DB, up securityUpdate, check *authenticatedMutation) error {
|
||||
if check != nil {
|
||||
if err := check.lockAndCheck(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if up.touched[settingSecAppURL] {
|
||||
if err := lockUsersForAuthChange(tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// 重读而非直接 Store 合并值:并发补丁各写各键,重读保证快照收敛到库内最终值
|
||||
return s.ReloadSecurity(ctx)
|
||||
if err := s.checkAppURLUpdateTx(tx, up); err != nil {
|
||||
return err
|
||||
}
|
||||
for key := range up.touched {
|
||||
if err := saveSettingTx(tx, key, up.values[key]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SettingService) checkAppURLUpdateTx(tx *gorm.DB, up securityUpdate) error {
|
||||
if !up.touched[settingSecAppURL] {
|
||||
return nil
|
||||
}
|
||||
oldApp, err := settingValueTx(tx, settingSecAppURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ensureAppURLKeepsLogin(tx, oldApp, up.next.AppURL)
|
||||
}
|
||||
|
||||
// ensureAppURLKeepsLogin 防自锁:密码登录禁用期间,面板地址是所有免密方式的
|
||||
// 运行时依赖——清空(生效值)会使通行密钥/钱包/OAuth 回调全部不可用,一律拒绝;
|
||||
// 域名变更会使通行密钥失效(RP ID 绑定域名),须留有钱包身份或可用外部登录兜底。
|
||||
func (s *SettingService) ensureAppURLKeepsLogin(tx *gorm.DB, oldURL, newURL string) error {
|
||||
off, err := settingValueTx(tx, settingSecPasswordLoginOff)
|
||||
if err != nil || off != "1" {
|
||||
return err
|
||||
}
|
||||
newEff := s.effectiveOf(newURL)
|
||||
if newEff == "" {
|
||||
return ErrProviderLastLogin
|
||||
}
|
||||
// origin(scheme://host)整体比较:scheme 变化同样使 WebAuthn origin 失配
|
||||
if originOf(newEff) == originOf(s.effectiveOf(oldURL)) {
|
||||
return nil
|
||||
}
|
||||
if n, err := identityProviderCountTx(tx, "wallet"); err != nil || n > 0 {
|
||||
return err
|
||||
}
|
||||
view, err := oauthViewTx(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ok, err := anyBoundUsableProviderTx(tx, patchedUsable(view, UpdateOAuthInput{}))
|
||||
if err != nil || ok {
|
||||
return err
|
||||
}
|
||||
return ErrProviderLastLogin
|
||||
}
|
||||
|
||||
// effectiveOf 计算给定 app_url 设置值的生效地址(设置优先,回退 PUBLIC_URL 环境变量)。
|
||||
func (s *SettingService) effectiveOf(v string) string {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
return s.envPublicURL
|
||||
}
|
||||
|
||||
func effectiveOriginTx(tx *gorm.DB, settings *SettingService) (string, error) {
|
||||
if settings == nil {
|
||||
return "", nil
|
||||
}
|
||||
appURL, err := settingValueTx(tx, settingSecAppURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return originOf(settings.effectiveOf(appURL)), nil
|
||||
}
|
||||
|
||||
// originOf 取 URL 的 origin(scheme://host);空串或解析失败按原串返回。
|
||||
func originOf(rawURL string) string {
|
||||
if rawURL == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil || u.Host == "" {
|
||||
return rawURL
|
||||
}
|
||||
return u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
// ReloadSecurity 从库加载安全设置到内存快照;进程启动时调用一次。
|
||||
|
||||
Reference in New Issue
Block a user