using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using HoneyBox.Admin.Data; using HoneyBox.Admin.Entities; using HoneyBox.Admin.Models.Auth; using HoneyBox.Admin.Models.Common; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; namespace HoneyBox.Admin.Services; /// /// 认证服务实现 /// public class AuthService : IAuthService { private readonly AdminDbContext _dbContext; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly ICaptchaService _captchaService; // 登录失败锁定配置 private const int MaxFailedAttempts = 5; private const int LockoutMinutes = 30; // Token配置 private const int RefreshTokenExpirationDays = 7; private const int AccessTokenExpirationMinutes = 30; public AuthService( AdminDbContext dbContext, IConfiguration configuration, ILogger logger, ICaptchaService captchaService) { _dbContext = dbContext; _configuration = configuration; _logger = logger; _captchaService = captchaService; } /// public async Task LoginAsync(LoginRequest request, string ipAddress) { // 1. 首先验证验证码(优先于密码校验)临时注释,上线在启用 #if !DEBUG if (!_captchaService.Validate(request.CaptchaKey, request.CaptchaCode)) { _logger.LogWarning("登录失败:验证码错误或已过期,用户名: {Username}", request.Username); throw new AdminException(AdminErrorCodes.CaptchaInvalid, "验证码错误或已过期"); } #endif // 2. 查找用户 var user = await _dbContext.AdminUsers .Include(u => u.Department) .Include(u => u.AdminUserRoles) .ThenInclude(ur => ur.Role) .FirstOrDefaultAsync(u => u.Username == request.Username); if (user == null) { _logger.LogWarning("登录失败:用户 {Username} 不存在", request.Username); throw new AdminException(AdminErrorCodes.InvalidCredentials, "用户名或密码错误"); } // 3. 检查账户是否被锁定 if (user.LockoutEnd.HasValue && user.LockoutEnd.Value > DateTime.Now) { var remainingMinutes = (int)(user.LockoutEnd.Value - DateTime.Now).TotalMinutes + 1; _logger.LogWarning("登录失败:用户 {Username} 账户已锁定,剩余 {Minutes} 分钟", request.Username, remainingMinutes); throw new AdminException(AdminErrorCodes.AccountLocked, $"账户已锁定,请在 {remainingMinutes} 分钟后重试"); } // 4. 检查账户状态 if (user.Status == 0) { _logger.LogWarning("登录失败:用户 {Username} 账户已禁用", request.Username); throw new AdminException(AdminErrorCodes.AccountDisabled, "账户已禁用"); } // 5. 验证密码 if (!VerifyPassword(request.Password, user.PasswordHash)) { // 增加失败次数 user.LoginFailCount++; // 检查是否需要锁定账户 if (user.LoginFailCount >= MaxFailedAttempts) { user.LockoutEnd = DateTime.Now.AddMinutes(LockoutMinutes); user.LoginFailCount = 0; _logger.LogWarning("用户 {Username} 登录失败次数过多,账户已锁定 {Minutes} 分钟", request.Username, LockoutMinutes); } await _dbContext.SaveChangesAsync(); var remainingAttempts = MaxFailedAttempts - user.LoginFailCount; _logger.LogWarning("登录失败:用户 {Username} 密码错误,剩余尝试次数 {Remaining}", request.Username, remainingAttempts); throw new AdminException(AdminErrorCodes.InvalidCredentials, "用户名或密码错误"); } // 6. 登录成功,重置失败次数 user.LoginFailCount = 0; user.LockoutEnd = null; user.LastLoginTime = DateTime.Now; user.LastLoginIp = ipAddress; // 7. 生成双Token var expireMinutes = _configuration.GetValue("Jwt:ExpireMinutes", AccessTokenExpirationMinutes); var accessToken = GenerateJwtToken(user, expireMinutes); var (refreshToken, refreshTokenEntity) = GenerateRefreshToken(user.Id, ipAddress); // 8. 保存RefreshToken到数据库 _dbContext.RefreshTokens.Add(refreshTokenEntity); await _dbContext.SaveChangesAsync(); // 9. 获取用户角色和权限 var roles = user.AdminUserRoles .Where(ur => ur.Role.Status == 1) .Select(ur => ur.Role.Code) .ToList(); var permissions = await GetUserPermissionsAsync(user.Id); _logger.LogInformation("用户 {Username} 登录成功,IP: {IpAddress}", request.Username, ipAddress); return new LoginResponse { AccessToken = accessToken, RefreshToken = refreshToken, ExpiresIn = expireMinutes * 60, UserInfo = new AdminUserInfo { Id = user.Id, Username = user.Username, RealName = user.RealName, Avatar = user.Avatar, Email = user.Email, Phone = user.Phone, DepartmentId = user.DepartmentId, DepartmentName = user.Department?.Name, Roles = roles, Permissions = permissions } }; } /// public async Task RefreshTokenAsync(string refreshToken, string ipAddress) { // 1. 计算Token哈希 var tokenHash = HashToken(refreshToken); // 2. 查找Token var storedToken = await _dbContext.RefreshTokens .Include(t => t.AdminUser) .ThenInclude(u => u.AdminUserRoles) .ThenInclude(ur => ur.Role) .FirstOrDefaultAsync(t => t.TokenHash == tokenHash); if (storedToken == null) { _logger.LogWarning("Token刷新失败:Token不存在"); throw new AdminException(AdminErrorCodes.InvalidRefreshToken, "无效的RefreshToken"); } // 3. 检查Token是否有效 if (!storedToken.IsActive) { if (storedToken.IsRevoked) { _logger.LogWarning("Token刷新失败:Token已被撤销,用户ID: {UserId}", storedToken.AdminUserId); throw new AdminException(AdminErrorCodes.RefreshTokenRevoked, "RefreshToken已被撤销"); } if (storedToken.IsExpired) { _logger.LogWarning("Token刷新失败:Token已过期,用户ID: {UserId}", storedToken.AdminUserId); throw new AdminException(AdminErrorCodes.RefreshTokenExpired, "RefreshToken已过期"); } } // 4. 检查用户状态 var user = storedToken.AdminUser; if (user.Status == 0) { _logger.LogWarning("Token刷新失败:用户账户已禁用,用户ID: {UserId}", user.Id); throw new AdminException(AdminErrorCodes.AccountDisabled, "账户已禁用"); } // 5. 生成新的Token(Token轮换) var expireMinutes = _configuration.GetValue("Jwt:ExpireMinutes", AccessTokenExpirationMinutes); var newAccessToken = GenerateJwtToken(user, expireMinutes); var (newRefreshToken, newRefreshTokenEntity) = GenerateRefreshToken(user.Id, ipAddress); // 6. 撤销旧Token并记录替换关系 storedToken.RevokedAt = DateTime.Now; storedToken.RevokedByIp = ipAddress; storedToken.ReplacedByToken = newRefreshTokenEntity.TokenHash; // 7. 保存新Token _dbContext.RefreshTokens.Add(newRefreshTokenEntity); await _dbContext.SaveChangesAsync(); _logger.LogInformation("用户 {UserId} Token刷新成功,IP: {IpAddress}", user.Id, ipAddress); return new RefreshTokenResponse { AccessToken = newAccessToken, RefreshToken = newRefreshToken, ExpiresIn = expireMinutes * 60 }; } /// public async Task RevokeTokenAsync(string refreshToken, string ipAddress) { // 1. 计算Token哈希 var tokenHash = HashToken(refreshToken); // 2. 查找Token var storedToken = await _dbContext.RefreshTokens .FirstOrDefaultAsync(t => t.TokenHash == tokenHash); if (storedToken == null) { _logger.LogWarning("Token撤销失败:Token不存在"); throw new AdminException(AdminErrorCodes.InvalidRefreshToken, "无效的RefreshToken"); } // 3. 撤销Token if (!storedToken.IsRevoked) { storedToken.RevokedAt = DateTime.Now; storedToken.RevokedByIp = ipAddress; await _dbContext.SaveChangesAsync(); } _logger.LogInformation("Token已撤销,用户ID: {UserId},IP: {IpAddress}", storedToken.AdminUserId, ipAddress); } /// public async Task RevokeAllTokensAsync(long adminUserId, string ipAddress) { // 查找用户所有活跃的Token var activeTokens = await _dbContext.RefreshTokens .Where(t => t.AdminUserId == adminUserId && t.RevokedAt == null) .ToListAsync(); var now = DateTime.Now; foreach (var token in activeTokens) { token.RevokedAt = now; token.RevokedByIp = ipAddress; } await _dbContext.SaveChangesAsync(); _logger.LogInformation("用户 {UserId} 的所有Token已撤销(共{Count}个),IP: {IpAddress}", adminUserId, activeTokens.Count, ipAddress); } /// public async Task LogoutAsync(long adminUserId) { // 注意:完整的登出应该通过RevokeTokenAsync来撤销RefreshToken // 这里保留原有逻辑,前端应该同时调用RevokeTokenAsync _logger.LogInformation("用户 {UserId} 退出登录", adminUserId); await Task.CompletedTask; } /// public async Task GetCurrentUserInfoAsync(long adminUserId) { var user = await _dbContext.AdminUsers .Include(u => u.Department) .Include(u => u.AdminUserRoles) .ThenInclude(ur => ur.Role) .FirstOrDefaultAsync(u => u.Id == adminUserId); if (user == null) { throw new AdminException(AdminErrorCodes.InvalidCredentials, "用户不存在"); } var roles = user.AdminUserRoles .Where(ur => ur.Role.Status == 1) .Select(ur => ur.Role.Code) .ToList(); var permissions = await GetUserPermissionsAsync(user.Id); return new AdminUserInfo { Id = user.Id, Username = user.Username, RealName = user.RealName, Avatar = user.Avatar, Email = user.Email, Phone = user.Phone, DepartmentId = user.DepartmentId, DepartmentName = user.Department?.Name, Roles = roles, Permissions = permissions }; } /// public async Task ChangePasswordAsync(long adminUserId, ChangePasswordRequest request) { var user = await _dbContext.AdminUsers.FindAsync(adminUserId); if (user == null) { throw new AdminException(AdminErrorCodes.InvalidCredentials, "用户不存在"); } // 验证旧密码 if (!VerifyPassword(request.OldPassword, user.PasswordHash)) { throw new AdminException(AdminErrorCodes.InvalidCredentials, "旧密码错误"); } // 更新密码 user.PasswordHash = HashPassword(request.NewPassword); user.UpdatedAt = DateTime.Now; await _dbContext.SaveChangesAsync(); _logger.LogInformation("用户 {UserId} 修改密码成功", adminUserId); } /// /// 生成 JWT Token /// private string GenerateJwtToken(AdminUser user, int expireMinutes) { var secret = _configuration["Jwt:Secret"] ?? throw new InvalidOperationException("JWT Secret not configured"); var issuer = _configuration["Jwt:Issuer"] ?? "HoneyBox.Admin"; var audience = _configuration["Jwt:Audience"] ?? "HoneyBox.Admin.Client"; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new List { new(ClaimTypes.NameIdentifier, user.Id.ToString()), new(ClaimTypes.Name, user.Username), new("real_name", user.RealName ?? ""), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; // 添加角色声明 foreach (var userRole in user.AdminUserRoles.Where(ur => ur.Role.Status == 1)) { claims.Add(new Claim(ClaimTypes.Role, userRole.Role.Code)); } var token = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, expires: DateTime.Now.AddMinutes(expireMinutes), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } /// /// 生成 Refresh Token /// /// 返回原始Token和Token实体 private (string Token, RefreshToken Entity) GenerateRefreshToken(long adminUserId, string ipAddress) { // 生成安全随机Token var randomBytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomBytes); var token = Convert.ToBase64String(randomBytes); // 创建Token实体 var refreshToken = new RefreshToken { AdminUserId = adminUserId, TokenHash = HashToken(token), ExpiresAt = DateTime.Now.AddDays(RefreshTokenExpirationDays), CreatedAt = DateTime.Now, CreatedByIp = ipAddress }; return (token, refreshToken); } /// /// 哈希Token(用于安全存储) /// private static string HashToken(string token) { using var sha256 = SHA256.Create(); var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token)); return Convert.ToBase64String(hashedBytes); } /// /// 获取用户权限列表 /// private async Task> GetUserPermissionsAsync(long adminUserId) { // 获取用户的所有角色ID var roleIds = await _dbContext.AdminUserRoles .Where(ur => ur.AdminUserId == adminUserId) .Select(ur => ur.RoleId) .ToListAsync(); // 获取角色关联的权限 var permissions = await _dbContext.RolePermissions .Where(rp => roleIds.Contains(rp.RoleId)) .Include(rp => rp.Permission) .Select(rp => rp.Permission.Code) .Distinct() .ToListAsync(); return permissions; } /// /// 哈希密码 /// public static string HashPassword(string password) { using var sha256 = SHA256.Create(); var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); return Convert.ToBase64String(hashedBytes); } /// /// 验证密码 /// private static bool VerifyPassword(string password, string passwordHash) { var hash = HashPassword(password); return hash == passwordHash; } }