调整 .gitignore 以适配 backends 目录结构
将 MiGu.Server 相关忽略规则迁移至 backends/MiGu.Server,并新增对 .tmp-build* 和 /.cursor/rules 的忽略,优化敏感数据与临时文件的管理。
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user