HaniBlindBox/server/HoneyBox/src/HoneyBox.Admin/Services/AuthService.cs
2026-01-18 13:55:07 +08:00

447 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
/// <summary>
/// 认证服务实现
/// </summary>
public class AuthService : IAuthService
{
private readonly AdminDbContext _dbContext;
private readonly IConfiguration _configuration;
private readonly ILogger<AuthService> _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<AuthService> logger,
ICaptchaService captchaService)
{
_dbContext = dbContext;
_configuration = configuration;
_logger = logger;
_captchaService = captchaService;
}
/// <inheritdoc />
public async Task<LoginResponse> 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<int>("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
}
};
}
/// <inheritdoc />
public async Task<RefreshTokenResponse> 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. 生成新的TokenToken轮换
var expireMinutes = _configuration.GetValue<int>("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
};
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
public async Task LogoutAsync(long adminUserId)
{
// 注意完整的登出应该通过RevokeTokenAsync来撤销RefreshToken
// 这里保留原有逻辑前端应该同时调用RevokeTokenAsync
_logger.LogInformation("用户 {UserId} 退出登录", adminUserId);
await Task.CompletedTask;
}
/// <inheritdoc />
public async Task<AdminUserInfo> 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
};
}
/// <inheritdoc />
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);
}
/// <summary>
/// 生成 JWT Token
/// </summary>
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<Claim>
{
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);
}
/// <summary>
/// 生成 Refresh Token
/// </summary>
/// <returns>返回原始Token和Token实体</returns>
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);
}
/// <summary>
/// 哈希Token用于安全存储
/// </summary>
private static string HashToken(string token)
{
using var sha256 = SHA256.Create();
var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(token));
return Convert.ToBase64String(hashedBytes);
}
/// <summary>
/// 获取用户权限列表
/// </summary>
private async Task<List<string>> 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;
}
/// <summary>
/// 哈希密码
/// </summary>
public static string HashPassword(string password)
{
using var sha256 = SHA256.Create();
var hashedBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
return Convert.ToBase64String(hashedBytes);
}
/// <summary>
/// 验证密码
/// </summary>
private static bool VerifyPassword(string password, string passwordHash)
{
var hash = HashPassword(password);
return hash == passwordHash;
}
}