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>
This commit is contained in:
2026-09-10 17:31:39 +08:00
co-authored by Claude Sonnet 4.6
parent 3059befcf5
commit 6aca0297bb
48 changed files with 3583 additions and 52 deletions
@@ -0,0 +1,49 @@
using System.Security.Cryptography;
using System.Text;
namespace Service.Implement
{
/// <summary>
/// 密码哈希工具(PBKDF2-SHA256,格式 salt$hash,盐随机 16 字节)
/// </summary>
public static class PasswordHelper
{
private const int Iterations = 10_000;
private const int SaltSize = 16;
private const int HashSize = 32;
/// <summary>生成密码哈希(salt$hash</summary>
public static string Hash(string password)
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
var hash = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, HashSize);
return $"{Convert.ToHexString(salt)}${Convert.ToHexString(hash)}";
}
/// <summary>校验密码是否匹配</summary>
public static bool Verify(string password, string? stored)
{
if (string.IsNullOrWhiteSpace(stored)) return false;
var parts = stored.Split('$');
if (parts.Length != 2) return false;
var salt = Convert.FromHexString(parts[0]);
var expected = Convert.FromHexString(parts[1]);
var actual = Rfc2898DeriveBytes.Pbkdf2(password, salt, Iterations, HashAlgorithmName.SHA256, expected.Length);
return CryptographicOperations.FixedTimeEquals(actual, expected);
}
/// <summary>生成随机盐字符串(供外部使用)</summary>
public static string NewSalt()
{
var salt = RandomNumberGenerator.GetBytes(SaltSize);
return Convert.ToHexString(salt).ToLowerInvariant();
}
/// <summary>SHA256(供 RefreshToken 等场景)</summary>
public static string Sha256(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
}
}