Files
Migu2.0/MiGu.Server/Auth/UserStore.cs
T
zhaowei.huangandCursor 42978930ca feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退
从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 18:16:34 +08:00

76 lines
3.3 KiB
C#

using System.Security.Cryptography;
using System.Text;
namespace MiGu.Server.Auth;
/// <summary>
/// 最简化的用户表(占位实现):内置 admin / ops 两个账号 + PBKDF2 哈希密码校验。
/// 真实生产应替换成 Microsoft.AspNetCore.Identity 或外接 LDAP / OAuth。
///
/// 安全要点(哪怕是占位也要做到):
/// - 密码不明文存储,启动期用 PBKDF2-SHA256(100k iter, 16B salt) 哈希;
/// - 密码 hash 比较走 <see cref="CryptographicOperations.FixedTimeEquals"/> 防时序攻击;
/// - 不允许「空用户名 = 空密码」之类的快捷绕过。
///
/// 默认账号:
/// admin / admin (Platform scope, role-admin)
/// ops / ops (RCSMonitor scope, role-ops)
/// 默认密码同名是为了**开发机一次启动就能登录**;生产部署务必通过环境变量
/// <c>PLATFORM__AUTH__USERS__&lt;USERNAME&gt;__PASSWORD</c> 改写或接入真实身份源。
/// </summary>
public sealed class UserStore
{
public sealed record UserRecord(
string Id,
string Username,
string DisplayName,
string DefaultScope,
IReadOnlyList<string> Roles,
byte[] Salt,
byte[] PasswordHash);
private readonly Dictionary<string, UserRecord> _users;
public UserStore(IConfiguration config, ILogger<UserStore> logger)
{
_users = new Dictionary<string, UserRecord>(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 等覆盖。");
}
}
/// <summary>用户名 / 密码校验。返回 null = 不存在或密码错。<b>不向调用方区分两种失败原因</b>,防用户名枚举。</summary>
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<string> 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);
}