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);
}
}