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);
}