using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Security.Cryptography; using System.Text; using Microsoft.IdentityModel.Tokens; namespace MiGu.Server.Auth; /// /// 平台 JWT 颁发与验签。Secret 从 appsettings.jsonJwt:Secret 读取; /// 若为占位值 "REPLACE_ME" 则启动期生成临时随机 secret 并在日志里强制告警, /// 防止生产部署忘改 secret 导致历史 hardcoded "simple-mock-secret" 风险复现。 /// public sealed class JwtIssuer { /// 占位 secret;启动期检测到时自动换成进程随机值,并在日志里 critical 告警。 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 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), }; } /// 颁发 access token。 写入私有 claim ops(空格分隔,便于后端 [Authorize] policy 解析)。 public string Issue(string userId, string username, string scope, IReadOnlyList roles, IReadOnlyList ops) { var now = DateTime.UtcNow; var claims = new List { 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())), }; foreach (var r in roles ?? Array.Empty()) 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); } /// 给框架的 JwtBearer middleware 用的验证参数(启动期注入到 AddJwtBearer)。 public TokenValidationParameters BuildValidationParameters() => _validation; private static string RandomBase64Secret(int byteLen) { var buf = RandomNumberGenerator.GetBytes(byteLen); return Convert.ToBase64String(buf); } }