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