using System.Security.Cryptography; using System.Text; namespace MiGu.Server.Auth; /// /// 最简化的用户表(占位实现):内置 admin / ops 两个账号 + PBKDF2 哈希密码校验。 /// 真实生产应替换成 Microsoft.AspNetCore.Identity 或外接 LDAP / OAuth。 /// /// 安全要点(哪怕是占位也要做到): /// - 密码不明文存储,启动期用 PBKDF2-SHA256(100k iter, 16B salt) 哈希; /// - 密码 hash 比较走 防时序攻击; /// - 不允许「空用户名 = 空密码」之类的快捷绕过。 /// /// 默认账号: /// admin / admin (Platform scope, role-admin) /// ops / ops (RCSMonitor scope, role-ops) /// 默认密码同名是为了**开发机一次启动就能登录**;生产部署务必通过环境变量 /// PLATFORM__AUTH__USERS__<USERNAME>__PASSWORD 改写或接入真实身份源。 /// public sealed class UserStore { public sealed record UserRecord( string Id, string Username, string DisplayName, string DefaultScope, IReadOnlyList Roles, byte[] Salt, byte[] PasswordHash); private readonly Dictionary _users; public UserStore(IConfiguration config, ILogger logger) { _users = new Dictionary(StringComparer.OrdinalIgnoreCase); // 1. 内置 admin / ops(密码可从 appsettings 覆盖) var adminPwd = config["Auth:Users:admin:Password"] ?? "admin"; var opsPwd = config["Auth:Users:ops:Password"] ?? "ops"; Add("u-admin", "admin", "系统管理员", "Platform", new[] { "role-admin", "role-platform-write" }, adminPwd); Add("u-ops", "ops", "运营人员", "RCSMonitor", new[] { "role-ops", "role-monitor-read" }, opsPwd); if (adminPwd == "admin" || opsPwd == "ops") { logger.LogWarning( "UserStore 使用默认弱密码(admin/admin 或 ops/ops)。生产环境务必通过 appsettings.Production.json " + "或环境变量 PLATFORM__AUTH__USERS__admin__PASSWORD 等覆盖。"); } } /// 用户名 / 密码校验。返回 null = 不存在或密码错。不向调用方区分两种失败原因,防用户名枚举。 public UserRecord? Verify(string username, string password) { if (string.IsNullOrWhiteSpace(username) || string.IsNullOrEmpty(password)) return null; if (!_users.TryGetValue(username, out var u)) return null; var hash = Pbkdf2(password, u.Salt); return CryptographicOperations.FixedTimeEquals(hash, u.PasswordHash) ? u : null; } public UserRecord? Find(string username) => _users.TryGetValue(username ?? "", out var u) ? u : null; private void Add(string id, string username, string displayName, string defaultScope, IReadOnlyList roles, string plainPassword) { var salt = RandomNumberGenerator.GetBytes(16); var hash = Pbkdf2(plainPassword, salt); _users[username] = new UserRecord(id, username, displayName, defaultScope, roles, salt, hash); } private static byte[] Pbkdf2(string password, byte[] salt) => Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterations: 100_000, HashAlgorithmName.SHA256, 32); }