从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。 Co-authored-by: Cursor <cursoragent@cursor.com>
76 lines
3.3 KiB
C#
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__<USERNAME>__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);
|
|
}
|