301 lines
12 KiB
C#
301 lines
12 KiB
C#
using F9WebAiTest.Application.Common;
|
|
using F9WebAiTest.Application.Dtos;
|
|
using F9WebAiTest.Application.Dtos.Auth;
|
|
using F9WebAiTest.Application.Interfaces.Security;
|
|
using F9WebAiTest.Domain.Common;
|
|
using F9WebAiTest.Domain.Entities.SYS.Departments;
|
|
using F9WebAiTest.Domain.Entities.SYS.RBAC;
|
|
using F9WebAiTest.Domain.Enums.SYS.RBAC;
|
|
using Microsoft.Extensions.Configuration;
|
|
using RefreshToken = F9WebAiTest.Domain.Entities.RefreshToken;
|
|
|
|
namespace F9WebAiTest.Application.Services.Auth;
|
|
|
|
public class AuthService : IAuthService
|
|
{
|
|
private readonly IRepository<User> _userRepo;
|
|
private readonly IRepository<UserRole> _userRoleRepo;
|
|
private readonly IRepository<RolePermission> _rolePermissionRepo;
|
|
private readonly IRepository<Permission> _permissionRepo;
|
|
private readonly IRepository<Menu> _menuRepo;
|
|
private readonly IRepository<Role> _roleRepo;
|
|
private readonly IRepository<Department> _deptRepo;
|
|
private readonly IRepository<RefreshToken> _refreshTokenRepo;
|
|
private readonly IJwtService _jwt;
|
|
private readonly IConfiguration _config;
|
|
private readonly IPasswordHasher _passwordHasher;
|
|
|
|
public AuthService(
|
|
IRepository<User> userRepo,
|
|
IRepository<UserRole> userRoleRepo,
|
|
IRepository<RolePermission> rolePermissionRepo,
|
|
IRepository<Permission> permissionRepo,
|
|
IRepository<Menu> menuRepo,
|
|
IRepository<Role> roleRepo,
|
|
IRepository<Department> deptRepo,
|
|
IRepository<RefreshToken> refreshTokenRepo,
|
|
IJwtService jwt,
|
|
IConfiguration config,
|
|
IPasswordHasher passwordHasher)
|
|
{
|
|
_userRepo = userRepo;
|
|
_userRoleRepo = userRoleRepo;
|
|
_rolePermissionRepo = rolePermissionRepo;
|
|
_permissionRepo = permissionRepo;
|
|
_menuRepo = menuRepo;
|
|
_roleRepo = roleRepo;
|
|
_deptRepo = deptRepo;
|
|
_refreshTokenRepo = refreshTokenRepo;
|
|
_jwt = jwt;
|
|
_config = config;
|
|
_passwordHasher = passwordHasher;
|
|
}
|
|
|
|
public async Task<LoginResultDto> LoginAsync(LoginDto dto)
|
|
{
|
|
var user = await _userRepo.FirstOrDefaultAsync(u => u.LoginName == dto.Username);
|
|
if (user == null || !VerifyPassword(dto.Password, user.PasswordHash))
|
|
throw new ServiceException("用户名或密码错误", 401);
|
|
|
|
if (!user.IsActive)
|
|
throw new ServiceException("账号已被禁用,请联系管理员", 403);
|
|
|
|
return await IssueTokensAsync(user);
|
|
}
|
|
|
|
public async Task<LoginResultDto> RefreshTokenAsync(string refreshToken)
|
|
{
|
|
var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
|
if (stored == null || stored.IsRevoked || stored.ExpiresAt < DateTime.UtcNow)
|
|
throw new ServiceException("刷新令牌无效或已过期", 401);
|
|
|
|
var user = await _userRepo.GetByIdAsync(stored.EmployeeId);
|
|
if (user == null || !user.IsActive)
|
|
throw new ServiceException("用户不存在或已禁用", 401);
|
|
|
|
// 撤销旧 token,签发新 token(旋转)
|
|
stored.IsRevoked = true;
|
|
await _refreshTokenRepo.UpdateAsync(stored);
|
|
|
|
return await IssueTokensAsync(user);
|
|
}
|
|
|
|
public async Task RevokeRefreshTokenAsync(string refreshToken)
|
|
{
|
|
var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
|
if (stored != null)
|
|
{
|
|
stored.IsRevoked = true;
|
|
await _refreshTokenRepo.UpdateAsync(stored);
|
|
await _refreshTokenRepo.SaveChangesAsync();
|
|
}
|
|
}
|
|
|
|
public async Task<EmployeeInfoDto> GetCurrentUserAsync(long userId)
|
|
{
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
if (user == null) throw new ServiceException("用户不存在", 404);
|
|
return await BuildUserInfoAsync(user);
|
|
}
|
|
|
|
public async Task<List<MenuDto>> GetUserMenusAsync(long userId)
|
|
{
|
|
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
|
.Select(ur => ur.RoleId).ToList();
|
|
|
|
// 超级管理员返回全部菜单
|
|
var isAdmin = await IsAdminAsync(userId);
|
|
|
|
List<Menu> menus;
|
|
if (isAdmin)
|
|
{
|
|
menus = await _menuRepo.FindAsync(m => m.IsActive);
|
|
}
|
|
else
|
|
{
|
|
// 通过 Permission 表关联菜单:RolePermission → Permission(ResourceType=菜单) → ResourceId(=Menu.Id)
|
|
var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId)))
|
|
.Select(rp => rp.PermissionId).Distinct().ToList();
|
|
var permittedMenuIds = (await _permissionRepo.FindAsync(p =>
|
|
permissionIds.Contains(p.Id) && p.ResourceType == Resource_Type_Enum.菜单))
|
|
.Select(p => p.ResourceId).Distinct().ToHashSet();
|
|
|
|
// 全部活跃菜单
|
|
var allMenus = await _menuRepo.FindAsync(m => m.IsActive);
|
|
var menuDict = allMenus.ToDictionary(m => m.Id);
|
|
|
|
// 有权限的菜单 + 它们的祖先目录(父/祖目录自动包含,保证菜单树完整,非管理员也能看到一级菜单)
|
|
var resultIds = new HashSet<long>();
|
|
foreach (var id in permittedMenuIds)
|
|
{
|
|
var currentId = id;
|
|
while (currentId != 0 && menuDict.ContainsKey(currentId) && resultIds.Add(currentId))
|
|
{
|
|
currentId = menuDict[currentId].ParentId;
|
|
}
|
|
}
|
|
|
|
menus = allMenus.Where(m => resultIds.Contains(m.Id)).ToList();
|
|
}
|
|
|
|
// 只返回目录和菜单(分组/页面),按排序
|
|
menus = menus.Where(m => m.MenuType != MenuType_Enum.默认)
|
|
.OrderBy(m => m.SortOrder).ToList();
|
|
|
|
return BuildMenuTree(menus);
|
|
}
|
|
|
|
public async Task<List<string>> GetUserPermissionsAsync(long userId)
|
|
{
|
|
var isAdmin = await IsAdminAsync(userId);
|
|
if (isAdmin)
|
|
{
|
|
// 超级管理员:返回所有权限单元 Code(含菜单权限 + 资源权限)
|
|
return (await _permissionRepo.FindAsync(p => !string.IsNullOrEmpty(p.Code)))
|
|
.Select(p => p.Code).Distinct().ToList();
|
|
}
|
|
|
|
// 普通用户:角色 → RolePermission → Permission → Code(统一权限模型)
|
|
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
|
.Select(ur => ur.RoleId).ToList();
|
|
var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId)))
|
|
.Select(rp => rp.PermissionId).Distinct().ToList();
|
|
return (await _permissionRepo.FindAsync(p => permissionIds.Contains(p.Id) && !string.IsNullOrEmpty(p.Code)))
|
|
.Select(p => p.Code).Distinct().ToList();
|
|
}
|
|
|
|
public async Task ChangePasswordAsync(long userId, ChangePasswordDto dto)
|
|
{
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
if (user == null) throw new ServiceException("用户不存在", 404);
|
|
if (!VerifyPassword(dto.OldPassword, user.PasswordHash))
|
|
throw new ServiceException("原密码错误");
|
|
if ((dto.NewPassword ?? "").Length < 6)
|
|
throw new ServiceException("新密码至少6位");
|
|
|
|
// 新密码与创建用户一致:BCrypt(MD5(明文))
|
|
user.SetPassword(_passwordHasher.HashPassword(_passwordHasher.GetMd5Hash(dto.NewPassword)));
|
|
await _userRepo.UpdateAsync(user);
|
|
await _userRepo.SaveChangesAsync();
|
|
}
|
|
|
|
public async Task UpdateProfileAsync(long userId, UpdateProfileDto dto)
|
|
{
|
|
var user = await _userRepo.GetByIdAsync(userId);
|
|
if (user == null) throw new ServiceException("用户不存在", 404);
|
|
|
|
user.UpdateProfile(dto.Nickname, dto.Avatar, dto.Email, dto.Phone, null);
|
|
await _userRepo.UpdateAsync(user);
|
|
await _userRepo.SaveChangesAsync();
|
|
}
|
|
|
|
// ---- 私有方法 ----
|
|
|
|
/// <summary>校验密码,兼容旧格式 BCrypt(明文) 与新格式 BCrypt(MD5(明文))</summary>
|
|
private bool VerifyPassword(string password, string passwordHash)
|
|
{
|
|
if (string.IsNullOrEmpty(passwordHash)) return false;
|
|
if (_passwordHasher.VerifyPassword(password, passwordHash)) return true;
|
|
return _passwordHasher.VerifyPassword(_passwordHasher.GetMd5Hash(password), passwordHash);
|
|
}
|
|
|
|
private async Task<LoginResultDto> IssueTokensAsync(User user)
|
|
{
|
|
var roles = await GetRoleCodesAsync(user.Id);
|
|
var accessToken = _jwt.GenerateAccessToken(user, roles);
|
|
var refreshToken = _jwt.GenerateRefreshToken();
|
|
var expireMinutes = int.Parse(_config["Jwt:AccessTokenExpireMinutes"] ?? "120");
|
|
|
|
// 存储 refresh token
|
|
var expireDays = int.Parse(_config["Jwt:RefreshTokenExpireDays"] ?? "7");
|
|
await _refreshTokenRepo.AddAsync(new RefreshToken
|
|
{
|
|
EmployeeId = user.Id,
|
|
Token = refreshToken,
|
|
ExpiresAt = DateTime.UtcNow.AddDays(expireDays)
|
|
});
|
|
await _refreshTokenRepo.SaveChangesAsync();
|
|
|
|
var userInfo = await BuildUserInfoAsync(user);
|
|
return new LoginResultDto
|
|
{
|
|
AccessToken = accessToken,
|
|
RefreshToken = refreshToken,
|
|
AccessTokenExpiresAt = DateTime.UtcNow.AddMinutes(expireMinutes),
|
|
User = userInfo
|
|
};
|
|
}
|
|
|
|
private async Task<List<string>> GetRoleCodesAsync(long userId)
|
|
{
|
|
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
|
.Select(ur => ur.RoleId).ToList();
|
|
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
|
return roles.Select(r => r.Code).ToList();
|
|
}
|
|
|
|
private async Task<bool> IsAdminAsync(long userId)
|
|
{
|
|
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
|
.Select(ur => ur.RoleId).ToList();
|
|
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
|
return roles.Any(r => r.Code == "admin");
|
|
}
|
|
|
|
private async Task<EmployeeInfoDto> BuildUserInfoAsync(User user)
|
|
{
|
|
var dept = await _deptRepo.GetByIdAsync(user.DepartmentId);
|
|
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == user.Id))
|
|
.Select(ur => ur.RoleId).ToList();
|
|
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
|
|
|
return new EmployeeInfoDto
|
|
{
|
|
Id = user.Id,
|
|
Username = user.LoginName,
|
|
RealName = user.UserName,
|
|
Nickname = user.Nickname,
|
|
Avatar = user.Avatar,
|
|
Phone = user.PhoneNumber,
|
|
Email = user.Email,
|
|
DepartmentId = user.DepartmentId,
|
|
DepartmentName = dept?.Name ?? "",
|
|
Status = user.IsActive ? 1 : 0,
|
|
Roles = roles.Select(r => r.Code).ToList(),
|
|
Permissions = await GetUserPermissionsAsync(user.Id)
|
|
};
|
|
}
|
|
|
|
private static List<MenuDto> BuildMenuTree(List<Menu> menus)
|
|
{
|
|
var dict = menus.ToDictionary(m => m.Id, m => new MenuDto
|
|
{
|
|
Id = m.Id,
|
|
ParentId = m.ParentId == 0 ? null : m.ParentId,
|
|
Name = m.Name,
|
|
Type = m.MenuType == MenuType_Enum.分组 ? "directory" : "menu",
|
|
Path = m.Path,
|
|
Component = m.Component,
|
|
Icon = m.IconSvg,
|
|
Permission = m.Permission,
|
|
Sort = m.SortOrder,
|
|
Status = m.IsActive ? 1 : 0
|
|
});
|
|
|
|
var roots = new List<MenuDto>();
|
|
foreach (var menu in menus)
|
|
{
|
|
var dto = dict[menu.Id];
|
|
if (menu.ParentId == 0 || !dict.ContainsKey(menu.ParentId))
|
|
{
|
|
roots.Add(dto);
|
|
}
|
|
else
|
|
{
|
|
dict[menu.ParentId].Children.Add(dto);
|
|
}
|
|
}
|
|
return roots.OrderBy(m => m.Sort).ToList();
|
|
}
|
|
}
|