调整 .gitignore 以适配 backends 目录结构

将 MiGu.Server 相关忽略规则迁移至 backends/MiGu.Server,并新增对 .tmp-build* 和 /.cursor/rules 的忽略,优化敏感数据与临时文件的管理。
This commit is contained in:
ArtoriasWu
2026-06-22 08:55:55 +08:00
parent 45f0261ed7
commit 87629a8537
46 changed files with 10 additions and 3 deletions
-100
View File
@@ -1,100 +0,0 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace MiGu.Server.Auth;
/// <summary>
/// 平台 JWT 颁发与验签。Secret 从 <c>appsettings.json</c> 的 <c>Jwt:Secret</c> 读取;
/// 若为占位值 <c>"REPLACE_ME"</c> 则启动期生成临时随机 secret 并在日志里强制告警,
/// 防止生产部署忘改 secret 导致历史 hardcoded "simple-mock-secret" 风险复现。
/// </summary>
public sealed class JwtIssuer
{
/// <summary>占位 secret;启动期检测到时自动换成进程随机值,并在日志里 critical 告警。</summary>
public const string PlaceholderSecret = "REPLACE_ME";
public string Issuer { get; }
public string Audience { get; }
public TimeSpan Lifetime { get; }
public string SecretInUse { get; }
public bool SecretIsEphemeral { get; }
private readonly SymmetricSecurityKey _key;
private readonly SigningCredentials _signing;
private readonly TokenValidationParameters _validation;
public JwtIssuer(string secret, string issuer, string audience, TimeSpan lifetime, ILogger<JwtIssuer> logger)
{
Issuer = issuer;
Audience = audience;
Lifetime = lifetime;
if (string.IsNullOrWhiteSpace(secret) || secret == PlaceholderSecret)
{
// 兜底:用进程随机 secret,保证签名不被预知,但 token 在 MiGu.Server 重启后失效。
// 这条路径只应出现在「开发机首次启动」/「忘改配置的部署」,**日志里强制告警让运维注意**。
secret = RandomBase64Secret(64);
SecretIsEphemeral = true;
logger.LogCritical(
"Jwt:Secret 是占位值或未配置 —— 已生成进程随机 secret(重启后所有 token 失效)。" +
"生产环境必须在 appsettings.Production.json 或环境变量 PLATFORM__JWT__SECRET 配置一个 ≥ 32 字节的稳定 secret。");
}
else if (Encoding.UTF8.GetByteCount(secret) < 32)
{
logger.LogWarning("Jwt:Secret 长度不足 32 字节,HS256 推荐 ≥ 32 字节 secret 以达到 256bit 强度。");
}
SecretInUse = secret;
_key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
_signing = new SigningCredentials(_key, SecurityAlgorithms.HmacSha256);
_validation = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ValidIssuer = Issuer,
ValidAudience = Audience,
IssuerSigningKey = _key,
ClockSkew = TimeSpan.FromSeconds(30),
};
}
/// <summary>颁发 access token。<paramref name="ops"/> 写入私有 claim <c>ops</c>(空格分隔,便于后端 <c>[Authorize]</c> policy 解析)。</summary>
public string Issue(string userId, string username, string scope, IReadOnlyList<string> roles, IReadOnlyList<string> ops)
{
var now = DateTime.UtcNow;
var claims = new List<Claim>
{
new(JwtRegisteredClaimNames.Sub, userId),
new(JwtRegisteredClaimNames.UniqueName, username),
new("scope", scope),
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")),
new(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
new("ops", string.Join(' ', ops ?? Array.Empty<string>())),
};
foreach (var r in roles ?? Array.Empty<string>())
claims.Add(new Claim(ClaimTypes.Role, r));
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
notBefore: now,
expires: now.Add(Lifetime),
signingCredentials: _signing);
return new JwtSecurityTokenHandler().WriteToken(token);
}
/// <summary>给框架的 JwtBearer middleware 用的验证参数(启动期注入到 AddJwtBearer)。</summary>
public TokenValidationParameters BuildValidationParameters() => _validation;
private static string RandomBase64Secret(int byteLen)
{
var buf = RandomNumberGenerator.GetBytes(byteLen);
return Convert.ToBase64String(buf);
}
}