using Serilog; using System.Security.Cryptography; using YZWater.Core.Models; namespace YZWater.Core.Services; /// /// 认证服务 - 用户登录、权限管理 /// public static class AuthService { private const int Pbkdf2Iterations = 100_000; private const int SaltSize = 16; private const int HashSize = 32; /// /// 最大连续登录失败次数,超过后锁定账户 /// private const int MaxFailedAttempts = 5; /// /// 账户锁定时长(分钟) /// private const int LockoutDurationMinutes = 15; private static User? _currentUser; private static DateTime _lastActivityTime = DateTime.Now; /// /// 当前登录用户 /// public static User? CurrentUser => _currentUser; /// /// 是否已登录(认证禁用时也视为已登录) /// public static bool IsLoggedIn => _currentUser != null || !IsAuthEnabled; /// /// 是否启用了用户认证(从配置文件读取,默认 true) /// public static bool IsAuthEnabled { get { var config = ConfigService.GetConfig(); return config?.EnableAuth ?? true; } } /// /// 会话超时时间(分钟) /// public static int SessionTimeoutMinutes { get; set; } = 30; /// /// 登录事件 /// public static event Action? UserLoggedIn; /// /// 登出事件 /// public static event Action? UserLoggedOut; /// /// 初始化:创建默认管理员账户(如果不存在) /// public static void Initialize() { try { var admin = DatabaseService.Db.Queryable() .Where(u => u.UserName == "admin") .First(); if (admin == null) { var defaultAdmin = new User { UserName = "admin", PasswordHash = HashPassword("admin123"), DisplayName = LanguageService.Instance.Get("AdminName"), Role = UserRole.Admin, IsActive = true, CreatedTime = DateTime.Now }; DatabaseService.Db.Insertable(defaultAdmin).ExecuteCommand(); Serilog.Log.Warning("已创建默认管理员账户 admin,请首次登录后立即修改默认密码"); } else { // 更新管理员显示名为当前语言 var currentDisplayName = LanguageService.Instance.Get("AdminName"); if (admin.DisplayName != currentDisplayName) { admin.DisplayName = currentDisplayName; DatabaseService.Db.Updateable(admin).UpdateColumns(u => u.DisplayName).ExecuteCommand(); } } } catch (Exception ex) { Log.Error(ex, "初始化用户账户失败"); } } /// /// 用户登录 /// public static (bool Success, string Message) Login(string userName, string password) { try { // 检查账户是否被锁定 if (IsAccountLocked(userName)) { var remainMin = GetLockoutRemainingMinutes(userName); Serilog.Log.Warning("登录拒绝:用户 {UserName} 账户已锁定,剩余 {Minutes} 分钟", userName, remainMin); AuditService.Log(userName, "Login", $"登录失败:账户已锁定(剩余{remainMin}分钟)", result: "Failed"); return (false, LanguageService.Instance.Get("AccountLocked")); } var user = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (user == null) { Serilog.Log.Warning("登录失败:用户 {UserName} 不存在", userName); RecordFailedAttempt(userName); AuditService.Log(userName, "Login", "登录失败:用户不存在", result: "Failed"); return (false, LanguageService.Instance.Get("InvalidCredentials")); } if (!user.IsActive) { Serilog.Log.Warning("登录失败:用户 {UserName} 已禁用", userName); RecordFailedAttempt(userName); AuditService.Log(userName, "Login", "登录失败:账户已禁用", result: "Failed"); return (false, LanguageService.Instance.Get("AccountDisabled")); } if (!VerifyPassword(password, user.PasswordHash)) { Serilog.Log.Warning("登录失败:用户 {UserName} 密码错误", userName); RecordFailedAttempt(userName); AuditService.Log(userName, "Login", "登录失败:密码错误", result: "Failed"); return (false, LanguageService.Instance.Get("InvalidCredentials")); } // 登录成功,清除失败记录 ClearFailedAttempts(userName); _currentUser = user; _lastActivityTime = DateTime.Now; if (NeedsPasswordRehash(user.PasswordHash)) { user.PasswordHash = HashPassword(password); DatabaseService.Db.Updateable(user) .UpdateColumns(u => u.PasswordHash) .ExecuteCommand(); Log.Information("用户 {UserName} 密码哈希已升级", userName); } // 更新最后登录时间 user.LastLoginTime = DateTime.Now; DatabaseService.Db.Updateable(user) .UpdateColumns(u => u.LastLoginTime) .ExecuteCommand(); AuditService.Log(userName, "Login", $"用户 {user.DisplayName}({user.Role}) 登录成功"); UserLoggedIn?.Invoke(user); Log.Information("用户 {UserName} 登录成功,角色: {Role}", userName, user.Role); return (true, LanguageService.Instance.Get("LoginSuccess")); } catch (Exception ex) { Log.Error(ex, "登录异常"); return (false, LanguageService.Instance.Get("LoginError")); } } /// /// 用户登出 /// public static void Logout() { if (_currentUser != null) { AuditService.Log(_currentUser.UserName, "Logout", "用户登出"); Log.Information("用户 {UserName} 已登出", _currentUser.UserName); } _currentUser = null; UserLoggedOut?.Invoke(); } /// /// 刷新活动时间(用于会话超时检测) /// public static void RefreshActivity() { _lastActivityTime = DateTime.Now; } /// /// 检查会话是否超时 /// public static bool IsSessionExpired() { return _currentUser != null && (DateTime.Now - _lastActivityTime).TotalMinutes >= SessionTimeoutMinutes; } /// /// 检查当前用户是否有指定权限(认证禁用时返回 true) /// public static bool HasPermission(UserRole requiredRole) { // 认证禁用模式:所有权限开放 if (!IsAuthEnabled) return true; if (_currentUser == null) return false; return _currentUser.Role >= requiredRole; } /// /// 修改密码 /// public static (bool Success, string Message) ChangePassword(string userName, string oldPassword, string newPassword) { try { var user = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (user == null) return (false, LanguageService.Instance.Get("UserNotFound")); if (!VerifyPassword(oldPassword, user.PasswordHash)) return (false, LanguageService.Instance.Get("OldPasswordWrong")); if (newPassword.Length < 6) return (false, LanguageService.Instance.Get("PasswordTooShort")); user.PasswordHash = HashPassword(newPassword); DatabaseService.Db.Updateable(user) .UpdateColumns(u => u.PasswordHash) .ExecuteCommand(); AuditService.Log(userName, "ChangePassword", "密码已修改"); Log.Information("用户 {UserName} 密码已修改", userName); return (true, LanguageService.Instance.Get("PasswordChanged")); } catch (Exception ex) { Log.Error(ex, "修改密码异常"); return (false, LanguageService.Instance.Get("ChangePasswordError")); } } /// /// 重置管理员密码(应急用) /// public static void ResetAdminPassword(string newPassword = "admin123") { try { var admin = DatabaseService.Db.Queryable().Where(u => u.UserName == "admin").First(); if (admin != null) { admin.PasswordHash = HashPassword(newPassword); DatabaseService.Db.Updateable(admin).UpdateColumns(u => u.PasswordHash).ExecuteCommand(); Serilog.Log.Information("管理员密码已重置"); } else { // 如果 admin 不存在,重新创建 Initialize(); } } catch (Exception ex) { Serilog.Log.Error(ex, "重置管理员密码失败"); } } /// /// 创建用户 /// public static (bool Success, string Message) CreateUser(string userName, string password, string displayName, UserRole role) { try { var existing = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (existing != null) return (false, LanguageService.Instance.Get("UserExists")); if (password.Length < 6) return (false, LanguageService.Instance.Get("PasswordTooShort")); var user = new User { UserName = userName, PasswordHash = HashPassword(password), DisplayName = displayName, Role = role, IsActive = true, CreatedTime = DateTime.Now }; DatabaseService.Db.Insertable(user).ExecuteCommand(); AuditService.Log(_currentUser?.UserName ?? "系统", "CreateUser", $"创建用户: {userName}, 角色: {role}"); Log.Information("已创建用户 {UserName}, 角色: {Role}", userName, role); return (true, LanguageService.Instance.Get("UserCreated")); } catch (Exception ex) { Log.Error(ex, "创建用户异常"); return (false, LanguageService.Instance.Get("CreateUserError")); } } /// /// 获取所有用户 /// public static List GetAllUsers() { return DatabaseService.Db.Queryable().ToList(); } /// /// 启用/禁用用户 /// public static void SetUserActive(string userName, bool isActive) { var user = DatabaseService.Db.Queryable().Where(u => u.UserName == userName).First(); if (user != null) { user.IsActive = isActive; DatabaseService.Db.Updateable(user).UpdateColumns(u => u.IsActive).ExecuteCommand(); AuditService.Log(_currentUser?.UserName ?? "系统", "SetUserActive", $"用户 {userName} 已{(isActive ? "启用" : "禁用")}"); } } // ─── 登录失败锁定(持久化到数据库,重启不丢失) ─── private static bool IsAccountLocked(string userName) { var user = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (user?.LockoutUntil == null) return false; // 锁定期已过,自动解锁(清零失败计数) if (user.LockoutUntil.Value <= DateTime.Now) { DatabaseService.Db.Updateable() .SetColumns(u => u.FailedLoginAttempts, 0) .SetColumns(u => u.LockoutUntil, null) .Where(u => u.Id == user.Id) .ExecuteCommand(); return false; } return true; } private static int GetLockoutRemainingMinutes(string userName) { var user = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (user?.LockoutUntil == null) return 0; var remaining = (user.LockoutUntil.Value - DateTime.Now).TotalMinutes; return Math.Max(0, (int)Math.Ceiling(remaining)); } private static void RecordFailedAttempt(string userName) { var user = DatabaseService.Db.Queryable() .Where(u => u.UserName == userName) .First(); if (user == null) return; var attempts = user.FailedLoginAttempts + 1; if (attempts >= MaxFailedAttempts) { DatabaseService.Db.Updateable() .SetColumns(u => u.FailedLoginAttempts, attempts) .SetColumns(u => u.LockoutUntil, DateTime.Now.AddMinutes(LockoutDurationMinutes)) .Where(u => u.Id == user.Id) .ExecuteCommand(); Log.Warning("用户 {UserName} 登录失败 {Attempts} 次,账户已锁定 {Minutes} 分钟", userName, attempts, LockoutDurationMinutes); } else { DatabaseService.Db.Updateable() .SetColumns(u => u.FailedLoginAttempts, attempts) .Where(u => u.Id == user.Id) .ExecuteCommand(); Log.Information("用户 {UserName} 登录失败 {Attempts}/{Max} 次", userName, attempts, MaxFailedAttempts); } } private static void ClearFailedAttempts(string userName) { DatabaseService.Db.Updateable() .SetColumns(u => u.FailedLoginAttempts, 0) .SetColumns(u => u.LockoutUntil, null) .Where(u => u.UserName == userName) .ExecuteCommand(); } /// /// 密码哈希(PBKDF2-SHA256,格式: pbkdf2$iterations$salt$hash) /// public static string HashPassword(string password) { var saltBytes = RandomNumberGenerator.GetBytes(SaltSize); var salt = Convert.ToBase64String(saltBytes); var hashBytes = Rfc2898DeriveBytes.Pbkdf2( password, saltBytes, Pbkdf2Iterations, HashAlgorithmName.SHA256, HashSize); var hash = Convert.ToBase64String(hashBytes); return $"pbkdf2${Pbkdf2Iterations}${salt}${hash}"; } /// /// 验证密码 /// public static bool VerifyPassword(string password, string? storedHash) { if (string.IsNullOrWhiteSpace(storedHash) || !storedHash.Contains('$')) { Serilog.Log.Warning("密码哈希格式无效"); return false; } if (storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal)) return VerifyPbkdf2(password, storedHash); return VerifyLegacySha256(password, storedHash); } private static bool NeedsPasswordRehash(string? storedHash) { if (string.IsNullOrWhiteSpace(storedHash)) return true; if (!storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal)) return true; var parts = storedHash.Split('$'); return parts.Length != 4 || !int.TryParse(parts[1], out var iterations) || iterations < Pbkdf2Iterations; } private static bool VerifyPbkdf2(string password, string storedHash) { var parts = storedHash.Split('$'); if (parts.Length != 4 || !int.TryParse(parts[1], out var iterations)) return false; try { var saltBytes = Convert.FromBase64String(parts[2]); var expectedBytes = Convert.FromBase64String(parts[3]); var actualBytes = Rfc2898DeriveBytes.Pbkdf2( password, saltBytes, iterations, HashAlgorithmName.SHA256, expectedBytes.Length); return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes); } catch (FormatException) { Log.Warning("密码哈希格式无效"); return false; } } private static bool VerifyLegacySha256(string password, string storedHash) { var parts = storedHash.Split('$', 2); if (parts.Length != 2) return false; var salt = parts[0]; var expectedHash = parts[1]; var actualHash = ComputeSha256(password, salt); return CryptographicOperations.FixedTimeEquals( System.Text.Encoding.UTF8.GetBytes(expectedHash), System.Text.Encoding.UTF8.GetBytes(actualHash)); } private static string ComputeSha256(string password, string salt) { using var sha256 = System.Security.Cryptography.SHA256.Create(); var combined = $"{salt}:{password}"; var bytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined)); return Convert.ToBase64String(bytes); } }