package api import ( "crypto/rand" "crypto/subtle" "crypto/tls" "encoding/json" "fmt" "net" "net/http" "strconv" "strings" "sync" "time" "github.com/gin-gonic/gin" "github.com/go-ldap/ldap/v3" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/service/ikev2" "vpn-platform/backend/internal/service/l2tp" "vpn-platform/backend/internal/service/openvpn" "vpn-platform/backend/internal/store" ) type pendingMFA struct { secret string backups []string expires time.Time } var ( pendingMFAMu sync.Mutex pendingMFAs = map[uint]pendingMFA{} ) func requireRole(roles ...string) gin.HandlerFunc { return func(c *gin.Context) { role := c.GetString("role") for _, allowed := range roles { if role == allowed { c.Next() return } } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "权限不足"}) } } func (s *Server) authenticateUser(username, password string) (*model.User, error) { var user model.User err := store.DB.Where("username = ?", username).First(&user).Error if err == nil && user.Source != "ldap" { if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)) == nil { return &user, nil } return nil, fmt.Errorf("invalid password") } if user.Source == "ldap" || err != nil { return s.authenticateLDAP(username, password) } return nil, err } func ldapSettings() (*model.LDAPConfig, error) { var cfg model.LDAPConfig if err := store.DB.First(&cfg).Error; err != nil || !cfg.Enabled { return nil, fmt.Errorf("LDAP 未启用") } plain, err := store.DecryptSecret(cfg.BindPassword) if err != nil { return nil, fmt.Errorf("LDAP 配置密钥无效") } cfg.BindPassword = plain if cfg.UsernameAttribute == "" { cfg.UsernameAttribute = "uid" } if cfg.UserFilter == "" { cfg.UserFilter = "(objectClass=inetOrgPerson)" } return &cfg, nil } func ldapConnect(cfg *model.LDAPConfig) (*ldap.Conn, error) { url := cfg.URL if !strings.HasPrefix(url, "ldap://") && !strings.HasPrefix(url, "ldaps://") { // 安全默认:未显式指定协议时按 ldaps,避免口令明文过网 url = "ldaps://" + url } conn, err := ldap.DialURL(url) if err != nil { return nil, err } if cfg.StartTLS && strings.HasPrefix(url, "ldap://") { host := strings.TrimPrefix(url, "ldap://") if h, _, err := net.SplitHostPort(host); err == nil { host = h } if err := conn.StartTLS(&tls.Config{ServerName: host}); err != nil { conn.Close() return nil, fmt.Errorf("StartTLS 升级失败: %w", err) } } else if strings.HasPrefix(url, "ldap://") { conn.Close() return nil, fmt.Errorf("明文 ldap:// 存在口令泄露风险,请改用 ldaps:// 或开启 StartTLS") } if cfg.BindDN != "" { if err := conn.Bind(cfg.BindDN, cfg.BindPassword); err != nil { conn.Close() return nil, err } } return conn, nil } func (s *Server) authenticateLDAP(username, password string) (*model.User, error) { cfg, err := ldapSettings() if err != nil || password == "" { return nil, fmt.Errorf("LDAP 认证不可用") } conn, err := ldapConnect(cfg) if err != nil { return nil, err } defer conn.Close() filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UsernameAttribute, ldap.EscapeFilter(username)) result, err := conn.Search(ldap.NewSearchRequest(cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 5, false, filter, []string{"dn", cfg.UsernameAttribute, "cn", "mail"}, nil)) if err != nil || len(result.Entries) != 1 { return nil, fmt.Errorf("LDAP 用户不存在") } entry := result.Entries[0] if err := conn.Bind(entry.DN, password); err != nil { return nil, fmt.Errorf("LDAP 密码错误") } var user model.User err = store.DB.Where("username = ?", username).First(&user).Error if err != nil { user = model.User{Username: username, Email: entry.GetAttributeValue("mail"), Source: "ldap", Role: "viewer", Enabled: true} if err := store.DB.Create(&user).Error; err != nil { return nil, err } } return &user, nil } func verifyMFA(user *model.User, code string) error { var device model.MFADevice if err := store.DB.Where("user_id = ? AND enabled = ?", user.ID, true).First(&device).Error; err != nil { return nil } code = strings.TrimSpace(code) if code == "" { return fmt.Errorf("需要有效的 MFA 验证码") } secret, err := store.DecryptSecret(device.Secret) if err != nil { return fmt.Errorf("MFA 配置无效") } if totp.Validate(code, secret) { return nil } // 备用码一次性消费(丢失验证器时恢复) if consumeAdminBackupCode(&device, code) { store.Audit(user.Username, "mfa_backup_used", user.Username, "使用备用码登录") return nil } return fmt.Errorf("需要有效的 MFA 验证码") } func consumeAdminBackupCode(device *model.MFADevice, code string) bool { plain, err := store.DecryptSecret(device.BackupCodes) if err != nil || plain == "" { return false } var codes []string if json.Unmarshal([]byte(plain), &codes) != nil { return false } normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(code), " ", "")) for i, bc := range codes { if subtle.ConstantTimeCompare([]byte(strings.ToUpper(bc)), []byte(normalized)) == 1 { codes = append(codes[:i], codes[i+1:]...) raw, _ := json.Marshal(codes) if enc, err := store.EncryptSecret(string(raw)); err == nil { store.DB.Model(device).Update("backup_codes", enc) } return true } } return false } func (s *Server) mfaStatus(c *gin.Context) { var device model.MFADevice err := store.DB.Where("user_id = ?", c.GetUint("user_id")).First(&device).Error c.JSON(200, gin.H{"enabled": err == nil && device.Enabled}) } func backupCodes() []string { codes := make([]string, 10) for i := range codes { raw := make([]byte, 5) _, _ = rand.Read(raw) codes[i] = fmt.Sprintf("%X", raw) } return codes } func (s *Server) mfaSetup(c *gin.Context) { var user model.User if err := store.DB.First(&user, c.GetUint("user_id")).Error; err != nil { c.JSON(404, gin.H{"error": "用户不存在"}) return } key, err := totp.Generate(totp.GenerateOpts{Issuer: "OPSX VPN", AccountName: user.Username}) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } pendingMFAMu.Lock() pendingMFAs[user.ID] = pendingMFA{secret: key.Secret(), backups: backupCodes(), expires: time.Now().Add(15 * time.Minute)} pendingMFAMu.Unlock() c.JSON(200, gin.H{"otpauth_url": key.URL(), "secret": key.Secret(), "expires_in": 900}) } func (s *Server) mfaEnable(c *gin.Context) { var req struct { Code string `json:"code"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } userID := c.GetUint("user_id") pendingMFAMu.Lock() pending, ok := pendingMFAs[userID] delete(pendingMFAs, userID) pendingMFAMu.Unlock() if !ok || time.Now().After(pending.expires) || !totp.Validate(req.Code, pending.secret) { c.JSON(400, gin.H{"error": "验证码无效或设置已过期"}) return } secret, _ := store.EncryptSecret(pending.secret) raw, _ := json.Marshal(pending.backups) backups, _ := store.EncryptSecret(string(raw)) device := model.MFADevice{UserID: userID, Secret: secret, BackupCodes: backups, Enabled: true} store.DB.Where("user_id = ?", userID).Assign(device).FirstOrCreate(&device) store.Audit(c.GetString("username"), "enable_mfa", c.GetString("username"), "") c.JSON(200, gin.H{"ok": true, "backup_codes": pending.backups}) } func (s *Server) mfaDisable(c *gin.Context) { store.DB.Where("user_id = ?", c.GetUint("user_id")).Delete(&model.MFADevice{}) store.Audit(c.GetString("username"), "disable_mfa", c.GetString("username"), "") c.JSON(200, gin.H{"ok": true}) } func (s *Server) listUsers(c *gin.Context) { var users []model.User store.DB.Order("id").Find(&users) c.JSON(200, users) } func (s *Server) createUser(c *gin.Context) { var req struct { Username, Password, Email, Role string Enabled bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || req.Username == "" || len(req.Password) < 8 { c.JSON(400, gin.H{"error": "用户名及至少 8 位的密码必填"}) return } if req.Role == "" { req.Role = "viewer" } if req.Role != "admin" && req.Role != "operator" && req.Role != "viewer" { c.JSON(400, gin.H{"error": "角色无效"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user := model.User{Username: req.Username, Password: string(hash), Email: req.Email, Role: req.Role, Source: "local", Enabled: req.Enabled} if err := store.DB.Create(&user).Error; err != nil { c.JSON(400, gin.H{"error": "用户创建失败:" + err.Error()}) return } store.Audit(c.GetString("username"), "create_user", user.Username, "") c.JSON(200, user) } func (s *Server) updateUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.User if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "用户不存在"}) return } var req struct { Email, Role, Password string Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } if req.Email != "" { user.Email = req.Email } if req.Role != "" { user.Role = req.Role } if req.Enabled != nil { user.Enabled = *req.Enabled } if req.Password != "" { if len(req.Password) < 8 { c.JSON(400, gin.H{"error": "密码至少 8 位"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) user.MustChangePassword = true // 管理员重置后强制改密 } store.DB.Save(&user) c.JSON(200, user) } func (s *Server) deleteUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) if uint(id) == c.GetUint("user_id") { c.JSON(400, gin.H{"error": "不能删除当前用户"}) return } store.DB.Delete(&model.User{}, id) c.JSON(200, gin.H{"ok": true}) } func (s *Server) getLDAPConfig(c *gin.Context) { var cfg model.LDAPConfig if store.DB.First(&cfg).Error != nil { c.JSON(200, gin.H{"enabled": false}) return } c.JSON(200, cfg) } func (s *Server) saveLDAPConfig(c *gin.Context) { var req struct { URL string `json:"url"` BindDN string `json:"bind_dn"` BindPassword string `json:"bind_password"` BaseDN string `json:"base_dn"` UserFilter string `json:"user_filter"` UsernameAttribute string `json:"username_attribute"` StartTLS bool `json:"starttls"` Enabled bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || (req.Enabled && (req.URL == "" || req.BaseDN == "")) { c.JSON(400, gin.H{"error": "LDAP 地址与 Base DN 必填"}) return } var cfg model.LDAPConfig store.DB.First(&cfg) cfg.URL, cfg.BindDN, cfg.BaseDN, cfg.UserFilter, cfg.UsernameAttribute, cfg.StartTLS, cfg.Enabled = req.URL, req.BindDN, req.BaseDN, req.UserFilter, req.UsernameAttribute, req.StartTLS, req.Enabled if req.BindPassword != "" { cfg.BindPassword, _ = store.EncryptSecret(req.BindPassword) } store.DB.Save(&cfg) c.JSON(200, gin.H{"ok": true}) } func (s *Server) testLDAP(c *gin.Context) { cfg, err := ldapSettings() if err == nil { conn, connErr := ldapConnect(cfg) if connErr == nil { conn.Close() c.JSON(200, gin.H{"ok": true}) return } err = connErr } c.JSON(400, gin.H{"error": "LDAP 连接失败: " + err.Error()}) } func (s *Server) listVPNUsers(c *gin.Context) { var users []model.VPNUser q := store.DB.Order("id desc") if id := c.Query("tunnel_id"); id != "" { q = q.Where("tunnel_id = ?", id) } q.Find(&users) c.JSON(200, users) } func (s *Server) createVPNUser(c *gin.Context) { var req struct { TunnelID uint `json:"tunnel_id"` Username string `json:"username"` DisplayName string `json:"display_name"` Email string `json:"email"` Source string `json:"source"` LDAPDN string `json:"ldap_dn"` Password string `json:"password"` Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil || req.TunnelID == 0 || req.Username == "" { c.JSON(400, gin.H{"error": "隧道和用户名必填"}) return } if req.Source == "" { req.Source = "local" } if req.Source != "local" && req.Source != "ldap" { c.JSON(400, gin.H{"error": "来源必须为 local 或 ldap"}) return } t, err := store.GetTunnel(req.TunnelID) if err != nil { c.JSON(404, gin.H{"error": "隧道不存在"}) return } switch t.Protocol { case "openvpn", "l2tp", "ikev2": default: c.JSON(400, gin.H{"error": "该协议不支持拨号用户(支持 OpenVPN / L2TP / IKEv2)"}) return } user := model.VPNUser{TunnelID: req.TunnelID, Username: req.Username, DisplayName: req.DisplayName, Email: req.Email, Source: req.Source, LDAPDN: req.LDAPDN, Enabled: true} if req.Enabled != nil { user.Enabled = *req.Enabled } if user.Source == "local" { if len(req.Password) < 8 { c.JSON(400, gin.H{"error": "本地拨号用户密码至少 8 位"}) return } hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) // L2TP/IKEv2 需要可逆拨号密钥;OpenVPN 仅用 bcrypt,同时存一份便于统一导出说明。 if enc, err := store.EncryptSecret(req.Password); err == nil { user.DialSecret = enc } } else if user.LDAPDN == "" { c.JSON(400, gin.H{"error": "LDAP 拨号用户必须填写 LDAP DN"}) return } if err := store.DB.Create(&user).Error; err != nil { c.JSON(400, gin.H{"error": "创建失败:" + err.Error()}) return } _ = refreshDialAuth(t) c.JSON(200, user) } func (s *Server) updateVPNUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } var req struct { DisplayName, Email, Password string Enabled *bool `json:"enabled"` } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } user.DisplayName, user.Email = req.DisplayName, req.Email if req.Enabled != nil { if !*req.Enabled && user.Enabled { revokeOpenVPNCert(&user) } user.Enabled = *req.Enabled } if req.Password != "" { hash, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) user.Password = string(hash) if enc, err := store.EncryptSecret(req.Password); err == nil { user.DialSecret = enc } } store.DB.Save(&user) if t, err := store.GetTunnel(user.TunnelID); err == nil { _ = refreshDialAuth(t) } c.JSON(200, user) } func (s *Server) deleteVPNUser(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error == nil { revokeOpenVPNCert(&user) tunnelID := user.TunnelID store.DB.Delete(&model.VPNUser{}, id) if t, err := store.GetTunnel(tunnelID); err == nil { _ = refreshDialAuth(t) } c.JSON(200, gin.H{"ok": true}) return } store.DB.Delete(&model.VPNUser{}, id) c.JSON(200, gin.H{"ok": true}) } func revokeOpenVPNCert(user *model.VPNUser) { if user.Certificate == "" { return } t, err := store.GetTunnel(user.TunnelID) if err != nil || t.Protocol != "openvpn" { return } if err := openvpn.New().RevokeClientCertificate(t, user); err != nil { store.Audit("system", "revoke_cert_failed", user.Username, err.Error()) } } // vpnUserMFASetup 为 OpenVPN 拨号用户生成 TOTP 密钥(待确认)。 func (s *Server) vpnUserMFASetup(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } t, err := store.GetTunnel(user.TunnelID) if err != nil || t.Protocol != "openvpn" { c.JSON(400, gin.H{"error": "仅 OpenVPN 拨号用户支持 MFA"}) return } // TOTP 标准参数(RFC 6238):SHA1、30s、6 位、base32,兼容主流 Authenticator key, err := totp.Generate(totp.GenerateOpts{ Issuer: "OPSX-OpenVPN", AccountName: fmt.Sprintf("%s@%s", user.Username, t.Name), Period: 30, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) if err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } pendingMFAMu.Lock() // 用 user_id 偏移避免与管理端 MFA 冲突:拨号用户用高位 key 空间 pendingMFAs[vpnUserPendingKey(user.ID)] = pendingMFA{ secret: key.Secret(), backups: backupCodes(), expires: time.Now().Add(15 * time.Minute), } pendingMFAMu.Unlock() c.JSON(200, gin.H{ "otpauth_url": key.URL(), "secret": key.Secret(), "period": 30, "digits": 6, "algorithm": "SHA1", "encoding": "base32", "expires_in": 900, "hint": "兼容主流 Authenticator 应用:扫描二维码或手动输入 base32 密钥", }) } func vpnUserPendingKey(vpnUserID uint) uint { // 管理端 MFA 用真实 user_id;拨号 MFA pending 用高位偏移隔离。 return 1_000_000_000 + vpnUserID } func (s *Server) vpnUserMFAEnable(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } var req struct { Code string `json:"code"` } if c.ShouldBindJSON(&req) != nil || req.Code == "" { c.JSON(400, gin.H{"error": "请输入验证码"}) return } pendingMFAMu.Lock() pending, ok := pendingMFAs[vpnUserPendingKey(user.ID)] delete(pendingMFAs, vpnUserPendingKey(user.ID)) pendingMFAMu.Unlock() if !ok || time.Now().After(pending.expires) || !totp.Validate(strings.TrimSpace(req.Code), pending.secret) { c.JSON(400, gin.H{"error": "验证码无效或设置已过期,请重新扫描二维码"}) return } secret, err := store.EncryptSecret(pending.secret) if err != nil { c.JSON(500, gin.H{"error": "加密 MFA 密钥失败"}) return } raw, _ := json.Marshal(pending.backups) backups, err := store.EncryptSecret(string(raw)) if err != nil { c.JSON(500, gin.H{"error": "加密备用码失败"}) return } user.MFASecret = secret user.MFABackupCodes = backups user.MFAEnabled = true if err := store.DB.Save(&user).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "enable_vpn_user_mfa", user.Username, "") c.JSON(200, gin.H{"ok": true, "backup_codes": pending.backups, "mfa_enabled": true}) } func (s *Server) vpnUserMFADisable(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } store.DB.Model(&user).Updates(map[string]interface{}{ "mfa_enabled": false, "mfa_secret": "", "mfa_backup_codes": "", "mfa_pin": "", "mfa_has_pin": false, "mfa_fail_count": 0, "mfa_locked_until": nil, "mfa_last_counter": 0, "mfa_last_used_at": nil, }) store.Audit(c.GetString("username"), "disable_vpn_user_mfa", user.Username, "") c.JSON(200, gin.H{"ok": true, "mfa_enabled": false}) } // vpnUserMFAStatus OTP Manager:查看拨号用户 MFA 状态。 func (s *Server) vpnUserMFAStatus(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } locked := user.MFALockedUntil != nil && time.Now().Before(*user.MFALockedUntil) c.JSON(200, gin.H{ "mfa_enabled": user.MFAEnabled, "mfa_has_pin": user.MFAHasPin, "mfa_fail_count": user.MFAFailCount, "mfa_locked": locked, "mfa_locked_until": user.MFALockedUntil, "mfa_last_used_at": user.MFALastUsedAt, "compatible": "TOTP 标准(兼容 Google/Microsoft Authenticator,sha1:base32:6:30s)", }) } // vpnUserMFAPin 设置或清除静态 PIN(连接时第二因子 = PIN+OTP)。 func (s *Server) vpnUserMFAPin(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } if !user.MFAEnabled { c.JSON(400, gin.H{"error": "请先启用 MFA 再设置 PIN"}) return } var req struct { Pin string `json:"pin"` // 空字符串表示清除 } if c.ShouldBindJSON(&req) != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } if err := openvpn.SetUserMFAPin(&user, req.Pin); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "set_vpn_user_mfa_pin", user.Username, "") c.JSON(200, gin.H{"ok": true, "mfa_has_pin": user.MFAHasPin}) } // vpnUserMFABackupCodes 重新生成备用码。 func (s *Server) vpnUserMFABackupCodes(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } codes, err := openvpn.RegenerateBackupCodes(&user) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "regen_vpn_user_mfa_backup", user.Username, "") c.JSON(200, gin.H{"ok": true, "backup_codes": codes}) } // vpnUserMFAUnlock 清除 MFA 锁定(失败次数过多后)。 func (s *Server) vpnUserMFAUnlock(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } if err := openvpn.UnlockUserMFA(&user); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "unlock_vpn_user_mfa", user.Username, "") c.JSON(200, gin.H{"ok": true}) } // vpnUserOTPSecretsLine 导出 TOTP 密钥行(标准 otp-secrets 格式,便于迁移与备份)。 func (s *Server) vpnUserOTPSecretsLine(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var user model.VPNUser if store.DB.First(&user, id).Error != nil { c.JSON(404, gin.H{"error": "拨号用户不存在"}) return } line, err := openvpn.GenerateOTPSecretsLine(&user) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } c.JSON(200, gin.H{ "line": line, "format": "user server type:hash:encoding:key:pin:udid client", "note": "标准 otp-secrets 格式密钥行;密钥敏感,请妥善保管,切勿泄露", }) } // refreshDialAuth 在拨号用户变更后重写 L2TP/IKEv2 认证文件。 func refreshDialAuth(t *model.Tunnel) error { if t == nil { return nil } switch t.Protocol { case "l2tp": return l2tp.New().RefreshAuth(t) case "ikev2": return ikev2.New().RefreshAuth(t) } return nil } // ---- 用户批量导入 / LDAP 导入 ---- // listLDAPUsers 浏览 LDAP 目录中的用户列表,供导入选择。复用 ldapSettings + ldapConnect。 func (s *Server) listLDAPUsers(c *gin.Context) { cfg, err := ldapSettings() if err != nil { c.JSON(400, gin.H{"error": "LDAP 未启用或配置不完整:" + err.Error()}) return } conn, err := ldapConnect(cfg) if err != nil { c.JSON(500, gin.H{"error": "LDAP 连接失败:" + err.Error()}) return } defer conn.Close() // 搜索 BaseDN 下所有匹配用户过滤器的条目 attrs := []string{"dn", cfg.UsernameAttribute, "cn", "mail"} result, err := conn.Search(ldap.NewSearchRequest( cfg.BaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, cfg.UserFilter, attrs, nil, )) if err != nil { c.JSON(500, gin.H{"error": "LDAP 搜索失败:" + err.Error()}) return } // 查询本地已有的 LDAP 用户名,标记已导入 var existingNames []string store.DB.Model(&model.User{}).Where("source = ?", "ldap").Pluck("username", &existingNames) existingMap := map[string]bool{} for _, n := range existingNames { existingMap[n] = true } type ldapUser struct { DN string `json:"dn"` UID string `json:"uid"` CN string `json:"cn"` Mail string `json:"mail"` Imported bool `json:"imported"` } users := make([]ldapUser, 0, len(result.Entries)) for _, entry := range result.Entries { uid := entry.GetAttributeValue(cfg.UsernameAttribute) if uid == "" { continue } users = append(users, ldapUser{ DN: entry.DN, UID: uid, CN: entry.GetAttributeValue("cn"), Mail: entry.GetAttributeValue("mail"), Imported: existingMap[uid], }) } c.JSON(200, users) } // batchCreateUsers 批量创建本地用户。 func (s *Server) batchCreateUsers(c *gin.Context) { var req struct { Users []struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` Role string `json:"role"` } `json:"users"` DefaultRole string `json:"default_role"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } if req.DefaultRole == "" { req.DefaultRole = "viewer" } type failInfo struct { Username string `json:"username"` Error string `json:"error"` } var success, skipped, failed int var fails []failInfo for _, u := range req.Users { if u.Username == "" { failed++ fails = append(fails, failInfo{Error: "用户名为空"}) continue } // 检查是否已存在 var count int64 store.DB.Model(&model.User{}).Where("username = ?", u.Username).Count(&count) if count > 0 { skipped++ continue } pwd := u.Password if len(pwd) < 8 { failed++ fails = append(fails, failInfo{Username: u.Username, Error: "密码至少 8 位"}) continue } role := u.Role if role == "" { role = req.DefaultRole } if role != "admin" && role != "operator" && role != "viewer" { role = "viewer" } hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) user := model.User{ Username: u.Username, Password: string(hash), Email: u.Email, Role: role, Source: "local", Enabled: true, } if err := store.DB.Create(&user).Error; err != nil { failed++ fails = append(fails, failInfo{Username: u.Username, Error: err.Error()}) continue } store.Audit(c.GetString("username"), "batch_create_user", u.Username, "") success++ } c.JSON(200, gin.H{ "success": success, "skipped": skipped, "failed": failed, "errors": fails, "total": len(req.Users), }) } // importLDAPUsers 从 LDAP 导入选中的用户到本地 DB(source=ldap)。 func (s *Server) importLDAPUsers(c *gin.Context) { var req struct { DNs []string `json:"dns"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) return } cfg, err := ldapSettings() if err != nil { c.JSON(400, gin.H{"error": "LDAP 未启用:" + err.Error()}) return } conn, err := ldapConnect(cfg) if err != nil { c.JSON(500, gin.H{"error": "LDAP 连接失败:" + err.Error()}) return } defer conn.Close() type failInfo struct { DN string `json:"dn"` Error string `json:"error"` } var success, skipped, failed int var fails []failInfo for _, dn := range req.DNs { // 按 DN 查询单个条目 result, err := conn.Search(ldap.NewSearchRequest( dn, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 0, 0, false, "(objectClass=*)", []string{cfg.UsernameAttribute, "cn", "mail"}, nil, )) if err != nil || len(result.Entries) == 0 { failed++ fails = append(fails, failInfo{DN: dn, Error: "查询失败"}) continue } entry := result.Entries[0] uid := entry.GetAttributeValue(cfg.UsernameAttribute) if uid == "" { failed++ fails = append(fails, failInfo{DN: dn, Error: "无 uid 属性"}) continue } // 检查本地是否已存在 var count int64 store.DB.Model(&model.User{}).Where("username = ?", uid).Count(&count) if count > 0 { skipped++ continue } user := model.User{ Username: uid, Email: entry.GetAttributeValue("mail"), Role: "viewer", Source: "ldap", Enabled: true, } if err := store.DB.Create(&user).Error; err != nil { failed++ fails = append(fails, failInfo{DN: dn, Error: err.Error()}) continue } store.Audit(c.GetString("username"), "import_ldap_user", uid, dn) success++ } c.JSON(200, gin.H{ "success": success, "skipped": skipped, "failed": failed, "errors": fails, "total": len(req.DNs), }) }