AuthService.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. using Serilog;
  2. using System.Security.Cryptography;
  3. using YZWater.Core.Models;
  4. namespace YZWater.Core.Services;
  5. /// <summary>
  6. /// 认证服务 - 用户登录、权限管理
  7. /// </summary>
  8. public static class AuthService
  9. {
  10. private const int Pbkdf2Iterations = 100_000;
  11. private const int SaltSize = 16;
  12. private const int HashSize = 32;
  13. /// <summary>
  14. /// 最大连续登录失败次数,超过后锁定账户
  15. /// </summary>
  16. private const int MaxFailedAttempts = 5;
  17. /// <summary>
  18. /// 账户锁定时长(分钟)
  19. /// </summary>
  20. private const int LockoutDurationMinutes = 15;
  21. private static User? _currentUser;
  22. private static DateTime _lastActivityTime = DateTime.Now;
  23. /// <summary>
  24. /// 当前登录用户
  25. /// </summary>
  26. public static User? CurrentUser => _currentUser;
  27. /// <summary>
  28. /// 是否已登录(认证禁用时也视为已登录)
  29. /// </summary>
  30. public static bool IsLoggedIn => _currentUser != null || !IsAuthEnabled;
  31. /// <summary>
  32. /// 是否启用了用户认证(从配置文件读取,默认 true)
  33. /// </summary>
  34. public static bool IsAuthEnabled
  35. {
  36. get
  37. {
  38. var config = ConfigService.GetConfig();
  39. return config?.EnableAuth ?? true;
  40. }
  41. }
  42. /// <summary>
  43. /// 会话超时时间(分钟)
  44. /// </summary>
  45. public static int SessionTimeoutMinutes { get; set; } = 30;
  46. /// <summary>
  47. /// 登录事件
  48. /// </summary>
  49. public static event Action<User>? UserLoggedIn;
  50. /// <summary>
  51. /// 登出事件
  52. /// </summary>
  53. public static event Action? UserLoggedOut;
  54. /// <summary>
  55. /// 初始化:创建默认管理员账户(如果不存在)
  56. /// </summary>
  57. public static void Initialize()
  58. {
  59. try
  60. {
  61. var admin = DatabaseService.Db.Queryable<User>()
  62. .Where(u => u.UserName == "admin")
  63. .First();
  64. if (admin == null)
  65. {
  66. var defaultAdmin = new User
  67. {
  68. UserName = "admin",
  69. PasswordHash = HashPassword("admin123"),
  70. DisplayName = LanguageService.Instance.Get("AdminName"),
  71. Role = UserRole.Admin,
  72. IsActive = true,
  73. CreatedTime = DateTime.Now
  74. };
  75. DatabaseService.Db.Insertable(defaultAdmin).ExecuteCommand();
  76. Serilog.Log.Warning("已创建默认管理员账户 admin,请首次登录后立即修改默认密码");
  77. }
  78. else
  79. {
  80. // 更新管理员显示名为当前语言
  81. var currentDisplayName = LanguageService.Instance.Get("AdminName");
  82. if (admin.DisplayName != currentDisplayName)
  83. {
  84. admin.DisplayName = currentDisplayName;
  85. DatabaseService.Db.Updateable(admin).UpdateColumns(u => u.DisplayName).ExecuteCommand();
  86. }
  87. }
  88. }
  89. catch (Exception ex)
  90. {
  91. Log.Error(ex, "初始化用户账户失败");
  92. }
  93. }
  94. /// <summary>
  95. /// 用户登录
  96. /// </summary>
  97. public static (bool Success, string Message) Login(string userName, string password)
  98. {
  99. try
  100. {
  101. // 检查账户是否被锁定
  102. if (IsAccountLocked(userName))
  103. {
  104. var remainMin = GetLockoutRemainingMinutes(userName);
  105. Serilog.Log.Warning("登录拒绝:用户 {UserName} 账户已锁定,剩余 {Minutes} 分钟", userName, remainMin);
  106. AuditService.Log(userName, "Login", $"登录失败:账户已锁定(剩余{remainMin}分钟)", result: "Failed");
  107. return (false, LanguageService.Instance.Get("AccountLocked"));
  108. }
  109. var user = DatabaseService.Db.Queryable<User>()
  110. .Where(u => u.UserName == userName)
  111. .First();
  112. if (user == null)
  113. {
  114. Serilog.Log.Warning("登录失败:用户 {UserName} 不存在", userName);
  115. RecordFailedAttempt(userName);
  116. AuditService.Log(userName, "Login", "登录失败:用户不存在", result: "Failed");
  117. return (false, LanguageService.Instance.Get("InvalidCredentials"));
  118. }
  119. if (!user.IsActive)
  120. {
  121. Serilog.Log.Warning("登录失败:用户 {UserName} 已禁用", userName);
  122. RecordFailedAttempt(userName);
  123. AuditService.Log(userName, "Login", "登录失败:账户已禁用", result: "Failed");
  124. return (false, LanguageService.Instance.Get("AccountDisabled"));
  125. }
  126. if (!VerifyPassword(password, user.PasswordHash))
  127. {
  128. Serilog.Log.Warning("登录失败:用户 {UserName} 密码错误", userName);
  129. RecordFailedAttempt(userName);
  130. AuditService.Log(userName, "Login", "登录失败:密码错误", result: "Failed");
  131. return (false, LanguageService.Instance.Get("InvalidCredentials"));
  132. }
  133. // 登录成功,清除失败记录
  134. ClearFailedAttempts(userName);
  135. _currentUser = user;
  136. _lastActivityTime = DateTime.Now;
  137. if (NeedsPasswordRehash(user.PasswordHash))
  138. {
  139. user.PasswordHash = HashPassword(password);
  140. DatabaseService.Db.Updateable(user)
  141. .UpdateColumns(u => u.PasswordHash)
  142. .ExecuteCommand();
  143. Log.Information("用户 {UserName} 密码哈希已升级", userName);
  144. }
  145. // 更新最后登录时间
  146. user.LastLoginTime = DateTime.Now;
  147. DatabaseService.Db.Updateable(user)
  148. .UpdateColumns(u => u.LastLoginTime)
  149. .ExecuteCommand();
  150. AuditService.Log(userName, "Login", $"用户 {user.DisplayName}({user.Role}) 登录成功");
  151. UserLoggedIn?.Invoke(user);
  152. Log.Information("用户 {UserName} 登录成功,角色: {Role}", userName, user.Role);
  153. return (true, LanguageService.Instance.Get("LoginSuccess"));
  154. }
  155. catch (Exception ex)
  156. {
  157. Log.Error(ex, "登录异常");
  158. return (false, LanguageService.Instance.Get("LoginError"));
  159. }
  160. }
  161. /// <summary>
  162. /// 用户登出
  163. /// </summary>
  164. public static void Logout()
  165. {
  166. if (_currentUser != null)
  167. {
  168. AuditService.Log(_currentUser.UserName, "Logout", "用户登出");
  169. Log.Information("用户 {UserName} 已登出", _currentUser.UserName);
  170. }
  171. _currentUser = null;
  172. UserLoggedOut?.Invoke();
  173. }
  174. /// <summary>
  175. /// 刷新活动时间(用于会话超时检测)
  176. /// </summary>
  177. public static void RefreshActivity()
  178. {
  179. _lastActivityTime = DateTime.Now;
  180. }
  181. /// <summary>
  182. /// 检查会话是否超时
  183. /// </summary>
  184. public static bool IsSessionExpired()
  185. {
  186. return _currentUser != null &&
  187. (DateTime.Now - _lastActivityTime).TotalMinutes >= SessionTimeoutMinutes;
  188. }
  189. /// <summary>
  190. /// 检查当前用户是否有指定权限(认证禁用时返回 true)
  191. /// </summary>
  192. public static bool HasPermission(UserRole requiredRole)
  193. {
  194. // 认证禁用模式:所有权限开放
  195. if (!IsAuthEnabled) return true;
  196. if (_currentUser == null) return false;
  197. return _currentUser.Role >= requiredRole;
  198. }
  199. /// <summary>
  200. /// 修改密码
  201. /// </summary>
  202. public static (bool Success, string Message) ChangePassword(string userName, string oldPassword, string newPassword)
  203. {
  204. try
  205. {
  206. var user = DatabaseService.Db.Queryable<User>()
  207. .Where(u => u.UserName == userName)
  208. .First();
  209. if (user == null)
  210. return (false, LanguageService.Instance.Get("UserNotFound"));
  211. if (!VerifyPassword(oldPassword, user.PasswordHash))
  212. return (false, LanguageService.Instance.Get("OldPasswordWrong"));
  213. if (newPassword.Length < 6)
  214. return (false, LanguageService.Instance.Get("PasswordTooShort"));
  215. user.PasswordHash = HashPassword(newPassword);
  216. DatabaseService.Db.Updateable(user)
  217. .UpdateColumns(u => u.PasswordHash)
  218. .ExecuteCommand();
  219. AuditService.Log(userName, "ChangePassword", "密码已修改");
  220. Log.Information("用户 {UserName} 密码已修改", userName);
  221. return (true, LanguageService.Instance.Get("PasswordChanged"));
  222. }
  223. catch (Exception ex)
  224. {
  225. Log.Error(ex, "修改密码异常");
  226. return (false, LanguageService.Instance.Get("ChangePasswordError"));
  227. }
  228. }
  229. /// <summary>
  230. /// 重置管理员密码(应急用)
  231. /// </summary>
  232. public static void ResetAdminPassword(string newPassword = "admin123")
  233. {
  234. try
  235. {
  236. var admin = DatabaseService.Db.Queryable<User>().Where(u => u.UserName == "admin").First();
  237. if (admin != null)
  238. {
  239. admin.PasswordHash = HashPassword(newPassword);
  240. DatabaseService.Db.Updateable(admin).UpdateColumns(u => u.PasswordHash).ExecuteCommand();
  241. Serilog.Log.Information("管理员密码已重置");
  242. }
  243. else
  244. {
  245. // 如果 admin 不存在,重新创建
  246. Initialize();
  247. }
  248. }
  249. catch (Exception ex)
  250. {
  251. Serilog.Log.Error(ex, "重置管理员密码失败");
  252. }
  253. }
  254. /// <summary>
  255. /// 创建用户
  256. /// </summary>
  257. public static (bool Success, string Message) CreateUser(string userName, string password, string displayName, UserRole role)
  258. {
  259. try
  260. {
  261. var existing = DatabaseService.Db.Queryable<User>()
  262. .Where(u => u.UserName == userName)
  263. .First();
  264. if (existing != null)
  265. return (false, LanguageService.Instance.Get("UserExists"));
  266. if (password.Length < 6)
  267. return (false, LanguageService.Instance.Get("PasswordTooShort"));
  268. var user = new User
  269. {
  270. UserName = userName,
  271. PasswordHash = HashPassword(password),
  272. DisplayName = displayName,
  273. Role = role,
  274. IsActive = true,
  275. CreatedTime = DateTime.Now
  276. };
  277. DatabaseService.Db.Insertable(user).ExecuteCommand();
  278. AuditService.Log(_currentUser?.UserName ?? "系统", "CreateUser", $"创建用户: {userName}, 角色: {role}");
  279. Log.Information("已创建用户 {UserName}, 角色: {Role}", userName, role);
  280. return (true, LanguageService.Instance.Get("UserCreated"));
  281. }
  282. catch (Exception ex)
  283. {
  284. Log.Error(ex, "创建用户异常");
  285. return (false, LanguageService.Instance.Get("CreateUserError"));
  286. }
  287. }
  288. /// <summary>
  289. /// 获取所有用户
  290. /// </summary>
  291. public static List<User> GetAllUsers()
  292. {
  293. return DatabaseService.Db.Queryable<User>().ToList();
  294. }
  295. /// <summary>
  296. /// 启用/禁用用户
  297. /// </summary>
  298. public static void SetUserActive(string userName, bool isActive)
  299. {
  300. var user = DatabaseService.Db.Queryable<User>().Where(u => u.UserName == userName).First();
  301. if (user != null)
  302. {
  303. user.IsActive = isActive;
  304. DatabaseService.Db.Updateable(user).UpdateColumns(u => u.IsActive).ExecuteCommand();
  305. AuditService.Log(_currentUser?.UserName ?? "系统", "SetUserActive",
  306. $"用户 {userName} 已{(isActive ? "启用" : "禁用")}");
  307. }
  308. }
  309. // ─── 登录失败锁定(持久化到数据库,重启不丢失) ───
  310. private static bool IsAccountLocked(string userName)
  311. {
  312. var user = DatabaseService.Db.Queryable<User>()
  313. .Where(u => u.UserName == userName)
  314. .First();
  315. if (user?.LockoutUntil == null) return false;
  316. // 锁定期已过,自动解锁(清零失败计数)
  317. if (user.LockoutUntil.Value <= DateTime.Now)
  318. {
  319. DatabaseService.Db.Updateable<User>()
  320. .SetColumns(u => u.FailedLoginAttempts, 0)
  321. .SetColumns(u => u.LockoutUntil, null)
  322. .Where(u => u.Id == user.Id)
  323. .ExecuteCommand();
  324. return false;
  325. }
  326. return true;
  327. }
  328. private static int GetLockoutRemainingMinutes(string userName)
  329. {
  330. var user = DatabaseService.Db.Queryable<User>()
  331. .Where(u => u.UserName == userName)
  332. .First();
  333. if (user?.LockoutUntil == null) return 0;
  334. var remaining = (user.LockoutUntil.Value - DateTime.Now).TotalMinutes;
  335. return Math.Max(0, (int)Math.Ceiling(remaining));
  336. }
  337. private static void RecordFailedAttempt(string userName)
  338. {
  339. var user = DatabaseService.Db.Queryable<User>()
  340. .Where(u => u.UserName == userName)
  341. .First();
  342. if (user == null) return;
  343. var attempts = user.FailedLoginAttempts + 1;
  344. if (attempts >= MaxFailedAttempts)
  345. {
  346. DatabaseService.Db.Updateable<User>()
  347. .SetColumns(u => u.FailedLoginAttempts, attempts)
  348. .SetColumns(u => u.LockoutUntil, DateTime.Now.AddMinutes(LockoutDurationMinutes))
  349. .Where(u => u.Id == user.Id)
  350. .ExecuteCommand();
  351. Log.Warning("用户 {UserName} 登录失败 {Attempts} 次,账户已锁定 {Minutes} 分钟",
  352. userName, attempts, LockoutDurationMinutes);
  353. }
  354. else
  355. {
  356. DatabaseService.Db.Updateable<User>()
  357. .SetColumns(u => u.FailedLoginAttempts, attempts)
  358. .Where(u => u.Id == user.Id)
  359. .ExecuteCommand();
  360. Log.Information("用户 {UserName} 登录失败 {Attempts}/{Max} 次",
  361. userName, attempts, MaxFailedAttempts);
  362. }
  363. }
  364. private static void ClearFailedAttempts(string userName)
  365. {
  366. DatabaseService.Db.Updateable<User>()
  367. .SetColumns(u => u.FailedLoginAttempts, 0)
  368. .SetColumns(u => u.LockoutUntil, null)
  369. .Where(u => u.UserName == userName)
  370. .ExecuteCommand();
  371. }
  372. /// <summary>
  373. /// 密码哈希(PBKDF2-SHA256,格式: pbkdf2$iterations$salt$hash)
  374. /// </summary>
  375. public static string HashPassword(string password)
  376. {
  377. var saltBytes = RandomNumberGenerator.GetBytes(SaltSize);
  378. var salt = Convert.ToBase64String(saltBytes);
  379. var hashBytes = Rfc2898DeriveBytes.Pbkdf2(
  380. password,
  381. saltBytes,
  382. Pbkdf2Iterations,
  383. HashAlgorithmName.SHA256,
  384. HashSize);
  385. var hash = Convert.ToBase64String(hashBytes);
  386. return $"pbkdf2${Pbkdf2Iterations}${salt}${hash}";
  387. }
  388. /// <summary>
  389. /// 验证密码
  390. /// </summary>
  391. public static bool VerifyPassword(string password, string? storedHash)
  392. {
  393. if (string.IsNullOrWhiteSpace(storedHash) || !storedHash.Contains('$'))
  394. {
  395. Serilog.Log.Warning("密码哈希格式无效");
  396. return false;
  397. }
  398. if (storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal))
  399. return VerifyPbkdf2(password, storedHash);
  400. return VerifyLegacySha256(password, storedHash);
  401. }
  402. private static bool NeedsPasswordRehash(string? storedHash)
  403. {
  404. if (string.IsNullOrWhiteSpace(storedHash)) return true;
  405. if (!storedHash.StartsWith("pbkdf2$", StringComparison.Ordinal)) return true;
  406. var parts = storedHash.Split('$');
  407. return parts.Length != 4 ||
  408. !int.TryParse(parts[1], out var iterations) ||
  409. iterations < Pbkdf2Iterations;
  410. }
  411. private static bool VerifyPbkdf2(string password, string storedHash)
  412. {
  413. var parts = storedHash.Split('$');
  414. if (parts.Length != 4 || !int.TryParse(parts[1], out var iterations)) return false;
  415. try
  416. {
  417. var saltBytes = Convert.FromBase64String(parts[2]);
  418. var expectedBytes = Convert.FromBase64String(parts[3]);
  419. var actualBytes = Rfc2898DeriveBytes.Pbkdf2(
  420. password,
  421. saltBytes,
  422. iterations,
  423. HashAlgorithmName.SHA256,
  424. expectedBytes.Length);
  425. return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes);
  426. }
  427. catch (FormatException)
  428. {
  429. Log.Warning("密码哈希格式无效");
  430. return false;
  431. }
  432. }
  433. private static bool VerifyLegacySha256(string password, string storedHash)
  434. {
  435. var parts = storedHash.Split('$', 2);
  436. if (parts.Length != 2) return false;
  437. var salt = parts[0];
  438. var expectedHash = parts[1];
  439. var actualHash = ComputeSha256(password, salt);
  440. return CryptographicOperations.FixedTimeEquals(
  441. System.Text.Encoding.UTF8.GetBytes(expectedHash),
  442. System.Text.Encoding.UTF8.GetBytes(actualHash));
  443. }
  444. private static string ComputeSha256(string password, string salt)
  445. {
  446. using var sha256 = System.Security.Cryptography.SHA256.Create();
  447. var combined = $"{salt}:{password}";
  448. var bytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combined));
  449. return Convert.ToBase64String(bytes);
  450. }
  451. }