| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- using Serilog;
- using System.Security.Cryptography;
- using YZWater.Core.Models;
- namespace YZWater.Core.Services;
- /// <summary>
- /// 认证服务 - 用户登录、权限管理
- /// </summary>
- public static class AuthService
- {
- private const int Pbkdf2Iterations = 100_000;
- private const int SaltSize = 16;
- private const int HashSize = 32;
- /// <summary>
- /// 最大连续登录失败次数,超过后锁定账户
- /// </summary>
- private const int MaxFailedAttempts = 5;
- /// <summary>
- /// 账户锁定时长(分钟)
- /// </summary>
- private const int LockoutDurationMinutes = 15;
- private static User? _currentUser;
- private static DateTime _lastActivityTime = DateTime.Now;
- /// <summary>
- /// 当前登录用户
- /// </summary>
- public static User? CurrentUser => _currentUser;
- /// <summary>
- /// 是否已登录(认证禁用时也视为已登录)
- /// </summary>
- public static bool IsLoggedIn => _currentUser != null || !IsAuthEnabled;
- /// <summary>
- /// 是否启用了用户认证(从配置文件读取,默认 true)
- /// </summary>
- public static bool IsAuthEnabled
- {
- get
- {
- var config = ConfigService.GetConfig();
- return config?.EnableAuth ?? true;
- }
- }
- /// <summary>
- /// 会话超时时间(分钟)
- /// </summary>
- public static int SessionTimeoutMinutes { get; set; } = 30;
- /// <summary>
- /// 登录事件
- /// </summary>
- public static event Action<User>? UserLoggedIn;
- /// <summary>
- /// 登出事件
- /// </summary>
- public static event Action? UserLoggedOut;
- /// <summary>
- /// 初始化:创建默认管理员账户(如果不存在)
- /// </summary>
- public static void Initialize()
- {
- try
- {
- var admin = DatabaseService.Db.Queryable<User>()
- .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, "初始化用户账户失败");
- }
- }
- /// <summary>
- /// 用户登录
- /// </summary>
- 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<User>()
- .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"));
- }
- }
- /// <summary>
- /// 用户登出
- /// </summary>
- public static void Logout()
- {
- if (_currentUser != null)
- {
- AuditService.Log(_currentUser.UserName, "Logout", "用户登出");
- Log.Information("用户 {UserName} 已登出", _currentUser.UserName);
- }
- _currentUser = null;
- UserLoggedOut?.Invoke();
- }
- /// <summary>
- /// 刷新活动时间(用于会话超时检测)
- /// </summary>
- public static void RefreshActivity()
- {
- _lastActivityTime = DateTime.Now;
- }
- /// <summary>
- /// 检查会话是否超时
- /// </summary>
- public static bool IsSessionExpired()
- {
- return _currentUser != null &&
- (DateTime.Now - _lastActivityTime).TotalMinutes >= SessionTimeoutMinutes;
- }
- /// <summary>
- /// 检查当前用户是否有指定权限(认证禁用时返回 true)
- /// </summary>
- public static bool HasPermission(UserRole requiredRole)
- {
- // 认证禁用模式:所有权限开放
- if (!IsAuthEnabled) return true;
- if (_currentUser == null) return false;
- return _currentUser.Role >= requiredRole;
- }
- /// <summary>
- /// 修改密码
- /// </summary>
- public static (bool Success, string Message) ChangePassword(string userName, string oldPassword, string newPassword)
- {
- try
- {
- var user = DatabaseService.Db.Queryable<User>()
- .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"));
- }
- }
- /// <summary>
- /// 重置管理员密码(应急用)
- /// </summary>
- public static void ResetAdminPassword(string newPassword = "admin123")
- {
- try
- {
- var admin = DatabaseService.Db.Queryable<User>().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, "重置管理员密码失败");
- }
- }
- /// <summary>
- /// 创建用户
- /// </summary>
- public static (bool Success, string Message) CreateUser(string userName, string password, string displayName, UserRole role)
- {
- try
- {
- var existing = DatabaseService.Db.Queryable<User>()
- .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"));
- }
- }
- /// <summary>
- /// 获取所有用户
- /// </summary>
- public static List<User> GetAllUsers()
- {
- return DatabaseService.Db.Queryable<User>().ToList();
- }
- /// <summary>
- /// 启用/禁用用户
- /// </summary>
- public static void SetUserActive(string userName, bool isActive)
- {
- var user = DatabaseService.Db.Queryable<User>().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<User>()
- .Where(u => u.UserName == userName)
- .First();
- if (user?.LockoutUntil == null) return false;
- // 锁定期已过,自动解锁(清零失败计数)
- if (user.LockoutUntil.Value <= DateTime.Now)
- {
- DatabaseService.Db.Updateable<User>()
- .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<User>()
- .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<User>()
- .Where(u => u.UserName == userName)
- .First();
- if (user == null) return;
- var attempts = user.FailedLoginAttempts + 1;
- if (attempts >= MaxFailedAttempts)
- {
- DatabaseService.Db.Updateable<User>()
- .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<User>()
- .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<User>()
- .SetColumns(u => u.FailedLoginAttempts, 0)
- .SetColumns(u => u.LockoutUntil, null)
- .Where(u => u.UserName == userName)
- .ExecuteCommand();
- }
- /// <summary>
- /// 密码哈希(PBKDF2-SHA256,格式: pbkdf2$iterations$salt$hash)
- /// </summary>
- 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}";
- }
- /// <summary>
- /// 验证密码
- /// </summary>
- 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);
- }
- }
|