c# - ASP.NET Core 3.1 JWT 签名在使用 AddJwtBearer() 时无效
问题描述
问题:AddJwtBearer()
失败,但手动验证令牌有效。
我正在尝试使用非对称 RSA 算法生成和验证 JWT。
我可以使用这个演示代码很好地生成 JWT
[HttpPost("[action]")]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> JwtBearerToken() {
AppUser user = await userManager.GetUserAsync(User);
using RSA rsa = RSA.Create(1024 * 2);
rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(rsa), SecurityAlgorithms.RsaSha256);
var jwt = new JwtSecurityToken(
audience: "identityapp",
issuer: "identityapp",
claims: new List<Claim>() {new Claim(ClaimTypes.NameIdentifier, user.UserName)},
notBefore: DateTime.Now,
expires: DateTime.Now.AddHours(3),
signingCredentials: signingCredentials
);
string token = new JwtSecurityTokenHandler().WriteToken(jwt);
return RedirectToAction(nameof(Index), new {jwt = token});
}
我还可以使用下面的演示代码验证令牌及其签名
[HttpPost("[action]")]
[ValidateAntiForgeryToken]
public IActionResult JwtBearerTokenVerify(string token) {
using RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(configuration["jwt:privateKey"]), out int _);
var handler = new JwtSecurityTokenHandler();
ClaimsPrincipal principal = handler.ValidateToken(token, new TokenValidationParameters() {
IssuerSigningKey = new RsaSecurityKey(rsa),
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
}, out SecurityToken securityToken);
return RedirectToAction(nameof(Index));
}
但是,当到达受保护的端点时,验证失败 (401)
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
来自 HTTP 标头的错误消息:Bearer error="invalid_token", error_description="签名无效"
我的 JWT 不记名身份验证配置在这里
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
using var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(Convert.FromBase64String(Configuration["jwt:privateKey"]), out int _);
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters() {
IssuerSigningKey = new RsaSecurityKey(rsa),
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
我可以使用对称密钥和 HmacSha256 轻松地使其工作——但这不是我想要的。
更新
我已经写了响应的异常,这就是我得到的:
IDX10503: Signature validation failed. Keys tried: 'Microsoft.IdentityModel.Tokens.RsaSecurityKey, KeyId: '', InternalId: '79b1afb2-0c85-43a1-bb81-e2accf9dff38'. , KeyId:
'.
Exceptions caught:
'System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'RSA'.
at System.Security.Cryptography.RSAImplementation.RSACng.ThrowIfDisposed()
at System.Security.Cryptography.RSAImplementation.RSACng.GetDuplicatedKeyHandle()
at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(ReadOnlySpan`1 hash, ReadOnlySpan`1 signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
at System.Security.Cryptography.RSAImplementation.RSACng.VerifyHash(Byte[] hash, Byte[] signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)
at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.VerifyWithRsa(Byte[] bytes, Byte[] signature)
at Microsoft.IdentityModel.Tokens.AsymmetricAdapter.Verify(Byte[] bytes, Byte[] signature)
at Microsoft.IdentityModel.Tokens.AsymmetricSignatureProvider.Verify(Byte[] input, Byte[] signature)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, TokenValidationParameters validationParameters)
at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters)
'.
token: '{"alg":"RS256","typ":"JWT"}.{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier":"mail@mail.com","nbf":1582878368,"exp":1582889168,"iss":"identityapp","aud":"identityapp"}'.
更新 - 工作解决方案
所以,我想我是从异常消息中弄清楚的。RSA 安全密钥被过早释放。
我从 中提取密钥创建AddJwtBearer()
,并改用依赖注入。
这似乎工作得很好。但我不确定这是否是好的做法。
// Somewhere futher up in the ConfigureServices(IServiceCollection services) method
services.AddTransient<RsaSecurityKey>(provider => {
RSA rsa = RSA.Create();
rsa.ImportRSAPrivateKey(
source: Convert.FromBase64String(Configuration["jwt:privateKey"]),
bytesRead: out int _);
return new RsaSecurityKey(rsa);
});
// Chaining onto services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => {
SecurityKey rsa = services.BuildServiceProvider().GetRequiredService<RsaSecurityKey>();
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters() {
IssuerSigningKey = rsa,
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
解决方案
虽然您的解决方案显然有效,但它有两个问题,我将提供解决方案。
第一个问题是RSA
您创建了 implements ,但是由于不是工厂的直接结果,因此IDisposable
在生命周期(此处为瞬态)内未正确处理处置。RSA
这会导致资源泄漏,其中未处理的 RSA 实例可能会在您的主机运行期间累积(甚至超出“正式”关闭时间)。
第二个问题是,BuildServiceProvider
除了其余代码隐式使用的服务提供者之外,您的使用还创建了一个全新的服务提供者。换句话说,这会创建一个与“规范”容器并行的新依赖注入容器。
解决方案如下。(注意我不能完美地测试你的场景,但我自己的应用程序中有类似的东西。)我将从中间的关键部分开始:
services
.AddTransient(provider => RSA.Create())
.AddTransient<SecurityKey>(provider =>
{
RSA rsa = provider.GetRequiredService<RSA>();
rsa.ImportRSAPrivateKey(source: Convert.FromBase64String(Configuration["jwt:privateKey"]), bytesRead: out int _);
return new RsaSecurityKey(rsa);
});
请注意如何RSA
获得自己的工厂。所以它在正确的时间被处理掉了。安全密钥也有自己的工厂,可以在需要时查找RSA
。
在我刚刚展示的代码上方的某个地方,您可以执行以下操作:
services
.AddOptions<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme)
.Configure<SecurityKey>((options, signingKey) =>
{
options.IncludeErrorDetails = true;
options.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = signingKey,
ValidAudience = "identityapp",
ValidIssuer = "identityapp",
RequireExpirationTime = true,
RequireAudience = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
};
});
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer();
请注意 是如何在方法TokenValidationParameters
中移动的Configure
!signingKey
将会有SecurityKey
你在依赖注入容器上注册的!这样我们就摆脱了BuildServiceProvider
.
警告:Microsoft 的 IdentityModel 似乎有一个错误,即在某些情况下,在第二个实例中使用一个RSA
,处理它,然后使用另一个会失败。例如,这是这个 SO question背后的根本问题。您可能会独立于我的解决方案而遇到该问题。但是您可以通过使用而不是添加您的(不一定是安全密钥)来回避这个问题。RSA
RSA
RSA
AddSingleton
AddTransient
推荐阅读
- c# - 如何在运行时更改 LuisService 的 BING SpellCheck?
- r - 包含三个随机数的随机集(在立方体中采样随机点)
- google-app-engine - 两个应用引擎之间的通信:“安全”环境中的灵活和标准
- java - 根据属性类型获取 XmlElement 或 XmlValue
- javascript - HtmlUnit获取编码错误的字符串
- c++ - 以自定义类为键的 Unordered_map
- r - ggplot2() 按因子绘制一个变量与自身的关系?
- javascript - 为什么这个 Javascript 可以在浏览器控制台中工作,但不能在 Selenium 的 JavascriptExecutor 中工作?
- python-3.x - 这是将 Keras 与 tensorflow 数据集一起使用的错误吗?
- javascript - Flow 是特定 JavaScript 版本的衍生产品吗?