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.json 的 Jwt: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);
}
}