Files
dengbolingandClaude Sonnet 4.6 6aca0297bb feat: 网关管理模块 + RBAC权限认证 + 组织架构 + 审计日志
- 网关管理:GatewayEntity/Service/Controller CRUD + 测试连接;设备 GatewayId 外键关联
- JWT 认证:登录签发 Token(权限编码写入 Claims)、RequirePermission 权限过滤器、CurrentUser 上下文
- RBAC:用户/角色/权限实体与 CRUD,预定义 4 角色 + 6 权限种子(admin/admin123)
- 组织架构:sys_org 固定层级树(公司/实验室/部门/班组白夜班),层级校验,用户挂 OrgId/岗位/技能标签
- 数据权限:角色 DataScope(全部/本组织及下级),用户列表按组织子树过滤
- 防锁死保护:禁止删/禁自己,保证至少一名活跃管理员,角色摘除 user:manage 前校验
- 审计日志:AuditRecorder 接入设备增删改/指令下发/登录登出/组织变更,AuditController 查询
- 设备指令下发按网关表取连接参数;设备列表支持 gatewayId/productId 筛选

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-09-10 17:31:39 +08:00

253 lines
11 KiB
C#

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Model;
using Model.Dto.System;
using Model.Entity.System;
using Model.Mapper;
using ORM;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
/// <summary>
/// 认证授权 服务实现:登录校验 → 加载角色/权限 → 签发 JWT(权限编码写入 Claims)
/// </summary>
public class AuthService : IAuthService
{
private readonly IConfiguration _configuration;
private readonly ICurrentUser _currentUser;
private readonly ILogger<AuthService> _logger;
public AuthService(IConfiguration configuration, ICurrentUser currentUser, ILogger<AuthService> logger)
{
_configuration = configuration;
_currentUser = currentUser;
_logger = logger;
}
public async Task<Result<LoginResultDto>> LoginAsync(LoginDto dto, string? ip)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.UserName) || string.IsNullOrWhiteSpace(dto.Password))
return Result<LoginResultDto>.Error("用户名和密码不能为空");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.UserName == dto.UserName && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户名或密码错误");
if (user.IsEnabled == 0)
return Result<LoginResultDto>.Error("账号已被禁用,请联系管理员");
if (!PasswordHelper.Verify(dto.Password, user.PasswordHash))
return Result<LoginResultDto>.Error("用户名或密码错误");
// 加载角色与权限
var (roleIds, roleNames, permissionCodes, dataScope) = await LoadUserAuthAsync(user.Id);
// 签发 JWT
var token = GenerateJwt(user, roleIds, roleNames, permissionCodes, dataScope);
// 更新最后登录时间
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.LastLoginTime == DateTime.Now)
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
// 登录审计
await WriteAuditAsync(user.Id, user.UserName, roleIds.FirstOrDefault(), roleNames.FirstOrDefault(),
"Login", "User", user.Id, 0, null, null, ip, $"用户 {user.UserName} 登录系统");
var userDto = user.ToDto();
userDto.RoleIds = roleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", roleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = token,
RefreshToken = token,
User = userDto,
Permissions = permissionCodes
});
}
catch (Exception ex)
{
_logger.LogError(ex, "登录异常");
return Result<LoginResultDto>.Error("登录失败", ex);
}
}
public async Task<Result> LogoutAsync()
{
if (!_currentUser.IsAuthenticated)
return Result.Success();
try
{
await WriteAuditAsync(_currentUser.UserId, _currentUser.UserName,
_currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Logout", "User", _currentUser.UserId, 0, null, null, null,
$"用户 {_currentUser.UserName} 退出登录");
return Result.Success();
}
catch (Exception ex)
{
_logger.LogError(ex, "退出登录记录审计失败");
return Result.Success(); // 退出本身不应失败
}
}
public async Task<Result<LoginResultDto>> GetMeAsync()
{
if (!_currentUser.IsAuthenticated)
return Result<LoginResultDto>.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result<LoginResultDto>.Error("用户不存在或已被删除");
var userDto = user.ToDto();
userDto.RoleIds = _currentUser.RoleIds.Select(r => r.ToString()).ToList();
userDto.RoleNames = string.Join(",", _currentUser.RoleNames);
return Result<LoginResultDto>.Success(new LoginResultDto
{
Token = "",
RefreshToken = "",
User = userDto,
Permissions = _currentUser.Permissions
});
}
catch (Exception ex)
{
return Result<LoginResultDto>.Error("获取当前用户信息失败", ex);
}
}
public async Task<Result> ChangePasswordAsync(ChangePasswordDto dto)
{
if (dto == null || string.IsNullOrWhiteSpace(dto.OldPassword) || string.IsNullOrWhiteSpace(dto.NewPassword))
return Result.Error("原密码和新密码不能为空");
if (dto.NewPassword.Length < 6)
return Result.Error("新密码长度不能少于 6 位");
if (!_currentUser.IsAuthenticated)
return Result.Error("未登录或 Token 已失效");
try
{
var user = await SqlSugarContext.DbContext.Queryable<UserEntity>()
.Where(x => x.Id == _currentUser.UserId && x.IsDel == 0).FirstAsync();
if (user == null)
return Result.Error("用户不存在或已被删除");
if (!PasswordHelper.Verify(dto.OldPassword, user.PasswordHash))
return Result.Error("原密码错误");
await SqlSugarContext.DbContext.Updateable<UserEntity>()
.SetColumns(x => x.PasswordHash == PasswordHelper.Hash(dto.NewPassword))
.Where(x => x.Id == user.Id).ExecuteCommandAsync();
await WriteAuditAsync(user.Id, user.UserName, _currentUser.RoleIds.FirstOrDefault(), _currentUser.RoleNames.FirstOrDefault(),
"Update", "User", user.Id, 0, null, null, null, $"用户 {user.UserName} 修改了自己的密码");
return Result.Success();
}
catch (Exception ex)
{
return Result.Error("修改密码失败", ex);
}
}
/// <summary>
/// 加载用户的角色Id/角色名/权限编码/数据范围(取所有角色的并集,数据范围取最宽)
/// </summary>
public static async Task<(List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)>
LoadUserAuthAsync(long userId)
{
var db = SqlSugarContext.DbContext;
var roleIds = await db.Queryable<UserRoleEntity>()
.Where(x => x.UserId == userId)
.Select(x => x.RoleId)
.ToListAsync();
var roleList = new List<RoleEntity>();
if (roleIds.Count > 0)
roleList = await db.Queryable<RoleEntity>()
.Where(x => roleIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
var permissionCodes = new List<string>();
if (roleIds.Count > 0)
{
var permIds = await db.Queryable<RolePermissionEntity>()
.Where(x => roleIds.Contains(x.RoleId))
.Select(x => x.PermissionId)
.ToListAsync();
if (permIds.Count > 0)
{
var perms = await db.Queryable<PermissionEntity>()
.Where(x => permIds.Contains(x.Id) && x.IsDel == 0).ToListAsync();
permissionCodes = perms.Select(p => p.Code).Distinct().ToList();
}
}
// 数据范围取最宽:任一角色是"全部"即为全部
var dataScope = roleList.Count > 0 ? roleList.Min(r => r.DataScope) : (byte)2;
return (roleIds, roleList.Select(r => r.Name).ToList(), permissionCodes, dataScope);
}
/// <summary>生成 JWT:把用户身份 + 角色 + 权限编码全部写入 Claims(授权过滤器免查库)</summary>
private string GenerateJwt(UserEntity user, List<long> roleIds, List<string> roleNames, List<string> permissions, byte dataScope)
{
var jwt = _configuration.GetSection("Jwt");
var secret = jwt["SecretKey"]!;
var issuer = jwt["Issuer"] ?? "IOT-API";
var audience = jwt["Audience"] ?? "IOT-Web";
var expireMinutes = double.TryParse(jwt["ExpireMinutes"], out var m) ? m : 480;
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
new(ClaimTypes.Name, user.UserName),
new("roleIds", string.Join(",", roleIds)),
new("roleNames", string.Join(",", roleNames)),
new("permissions", string.Join(",", permissions)),
new("dataScope", dataScope.ToString()),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N"))
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer, audience, claims,
expires: DateTime.Now.AddMinutes(expireMinutes), signingCredentials: creds);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>写一条审计日志</summary>
private static async Task WriteAuditAsync(long userId, string? userName, long? roleId, string? roleName,
string operationType, string operationTarget, long targetId, long deviceId,
string? oldValue, string? newValue, string? ip, string? description)
{
var now = DateTime.Now;
await SqlSugarContext.DbContext.Insertable(new AuditLogEntity
{
UserId = userId,
UserName = userName ?? "",
RoleId = roleId ?? 0,
RoleName = roleName ?? "",
OperationType = operationType,
OperationTarget = operationTarget,
TargetId = targetId,
DeviceId = deviceId,
OldValue = oldValue,
NewValue = newValue,
Ip = ip,
Description = description,
OperateTime = now,
CreateTime = now
}).ExecuteCommandAsync();
}
}
}