.NET 6升级至.NET 8后JWT令牌创建与解析报错求助
问题描述
将ASP.NET Core 6 Web API升级到.NET 8后,JWT令牌创建与解析先后出现两个错误:
- 创建令牌时触发错误:
System.ArgumentOutOfRangeException: 'IDX10720: Unable to create KeyedHashAlgorithm for algorithm 'HS256', the key size must be greater than: '256' bits, key has '152' bits. (Parameter 'keyBytes')'
加长密钥后令牌可正常生成,但解析时又出现新错误:
System.ArgumentException: 'IDX12723: Unable to decode the payload '[PII of type 'System.String' is hidden. For more details, see https://aka.ms/IdentityModel/PII.]' as Base64Url encoded string.'
内部异常信息:
JsonException: IDX11020: The JSON value of type: 'String', could not be converted to 'JsonTokenType.Number'. Reading: 'System.IdentityModel.Tokens.Jwt.JwtPayload.iat', Position: '52', CurrentDepth: '1', BytesConsumed: '75'.
相关代码
解析令牌的私有方法
private User? _GetIdentity(HttpContext context, string token) { try { var handler = new JwtSecurityTokenHandler(); //var jsonToken = handler.ReadToken(token); var tokenS = handler.ReadToken(token) as JwtSecurityToken;//error happens here var user = new User { Id = tokenS.Claims.First(claim => claim.Type == ClaimTypes.NameIdentifier).Value, UserName = tokenS.Claims.First(claim => claim.Type == ClaimTypes.Name).Value, Email = tokenS.Claims.First(claim => claim.Type == ClaimTypes.Email).Value, Role = tokenS.Claims.First(claim => claim.Type == ClaimTypes.Role).Value }; if (tokenS.ValidTo < DateTime.UtcNow) { throw new Exceptions.UnauthorizedAccessException($"..."); } return user; } catch(Exceptions.UnauthorizedAccessException e) { _logger.LogError(e.Message); throw; } }
JWT令牌生成类
public class JwtService { private const int EXPIRATION_TOKEN_MINUTES = 25; private const int EXPIRATION_REFRESH_DAYS = 7; private readonly IConfiguration _configuration; public JwtService(IConfiguration configuration) { _configuration = configuration; } public AuthenticationResponse CreateToken(IdentityUser user, IEnumerable<string> roles) { var expiration = DateTime.Now.AddMinutes(EXPIRATION_TOKEN_MINUTES); var refreshExpiration = DateTime.Now.AddDays(EXPIRATION_REFRESH_DAYS); var token = CreateJwtToken( CreateClaims(user, roles), CreateSigningCredentials(), expiration ); var refreshToken = CreateJwtToken( CreateRefreshTokenClaims(user.Id), CreateSigningCredentials(), refreshExpiration ); var tokenHandler = new JwtSecurityTokenHandler(); return new AuthenticationResponse { Token = tokenHandler.WriteToken(token), RefreshToken = tokenHandler.WriteToken(refreshToken), Expiration = expiration }; } private JwtSecurityToken CreateJwtToken(Claim[] claims, SigningCredentials credentials, DateTime expiration) => new JwtSecurityToken( _configuration["Jwt:Issuer"], _configuration["Jwt:Audience"], claims, expires: expiration, signingCredentials: credentials ); private Claim[] CreateRefreshTokenClaims(string userId) => new[] { new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), new Claim(ClaimTypes.NameIdentifier, userId) }; private Claim[] CreateClaims(IdentityUser user, IEnumerable<string> roles) => new[] { //new Claim(JwtRegisteredClaimNames.Sub, _configuration["Jwt:Subject"]), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Name, user.UserName), new Claim(ClaimTypes.Email, user.Email), } .Concat(roles.Select(r => new Claim(ClaimTypes.Role, r)).ToArray()) .ToArray(); private SigningCredentials CreateSigningCredentials() => new SigningCredentials( new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]) ), SecurityAlgorithms.HmacSha256 ); public ClaimsIdentity CreateClaimsIdentity(string username, string userId, IEnumerable<string> roles) { List<Claim> claims = new List<Claim> { new Claim(ClaimTypes.Name, username), new Claim(ClaimTypes.NameIdentifier, userId) }; claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role))); ClaimsIdentity identity = new ClaimsIdentity( claims, "Token", ClaimTypes.Name, ClaimTypes.Role ); return identity; } public ClaimsPrincipal ValidateToken(string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); // Set the token validation parameters var validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = _configuration["Jwt:Audience"], ValidIssuer = _configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration["Jwt:Secret"]) ) }; // Validate and parse the token var principal = tokenHandler.ValidateToken(token, validationParameters, out var _); return principal; } catch { // Token validation failed return null; } } }
问题原因与解决方案
1. HS256密钥长度的强制校验
.NET 8对JWT签名算法的密钥长度要求做了严格化处理:
- .NET 6中,HS256算法的密钥长度校验是宽松的,即使密钥不足256位(32字节)也能生成令牌;
- .NET 8开始严格遵循JWT标准,HS256要求密钥至少256位,否则直接抛出IDX10720错误。
你加长密钥的操作是正确的,无需额外调整。
2. 标准声明iat的类型校验增强
JWT标准明确规定iat(签发时间)必须是数字类型的Unix时间戳,但你的代码中错误地将其设置为字符串格式的DateTime:
// 错误写法 new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString())
- .NET 6的解析逻辑对类型校验较宽松,能容忍字符串格式的
iat; - .NET 8严格遵循标准,会校验
iat的类型,字符串格式的iat会触发IDX11020类型转换错误。
修复代码:将CreateClaims和CreateRefreshTokenClaims中的iat声明改为Unix时间戳(秒级):
// CreateClaims方法中修改iat声明 new Claim(JwtRegisteredClaimNames.Iat, ((DateTimeOffset)DateTime.UtcNow).ToUnixTimeSeconds().ToString()) // CreateRefreshTokenClaims方法中修改iat声明 new Claim(JwtRegisteredClaimNames.Iat, ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds().ToString())
注:这里最终仍是字符串,但内容为数字格式,符合JWT标准对iat的类型要求。
额外建议
建议使用JwtService.ValidateToken方法解析令牌,而非直接调用ReadToken。ValidateToken会自动处理标准声明的校验、类型转换及过期检查,比手动解析更安全规范。
内容的提问来源于stack exchange,提问作者Quak_2023

