c# - 在 .net 核心应用程序上实现 JWT 会引发无效负载
问题描述
我正在尝试在我的 .net core 2.2 API 和 .net core 3.1 Web-Application 之间实现 JWT 令牌,但是在登录后取消保护令牌时遇到了问题。在调试模式(本地计算机)下,它运行良好,在将其发布到我的服务器后,我立即遇到了各种异常。
首先我得到了例外The key {...some GUID...} was not found in the keyring
。所以我阅读并看到,我需要将Load User Profile
我的应用程序池设置为true
. 这就是我所做的。
现在我的例外说
CryptographicException: The payload was invalid.
Microsoft.AspNetCore.DataProtection.Cng.CbcAuthenticatedEncryptor.DecryptImpl(Byte* pbCiphertext, uint cbCiphertext, Byte* pbAdditionalAuthenticatedData, uint cbAdditionalAuthenticatedData)
Microsoft.AspNetCore.DataProtection.Cng.Internal.CngAuthenticatedEncryptorBase.Decrypt(ArraySegment<byte> ciphertext, ArraySegment<byte> additionalAuthenticatedData)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.UnprotectCore(byte[] protectedData, bool allowOperationsOnRevokedKeys, out UnprotectStatus status)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors, out bool requiresMigration, out bool wasRevoked)
Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector.Unprotect(byte[] protectedData)
KlehnPhotography.Site.TokenAuthentication.JwtTokenService.UnprotectToken(string protectedText) in JwtTokenValidator.cs
KlehnPhotography.Site.Service.ServiceBase.CreateClient() in ServiceBase.cs
KlehnPhotography.Site.Service.TagService.GetTags() in TagService.cs
KlehnPhotography.Site.Controllers.PhotosController.Index() in PhotosController.cs
lambda_method(Closure , object )
Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable+Awaiter.GetResult()
Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor+TaskOfActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, object controller, object[] arguments)
System.Threading.Tasks.ValueTask<TResult>.get_Result()
System.Runtime.CompilerServices.ValueTaskAwaiter<TResult>.GetResult()
Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.<InvokeActionMethodAsync>g__Awaited|12_0(ControllerActionInvoker invoker, ValueTask<IActionResult> actionResultValueTask)
....
我不知道从哪里开始,因为它在本地工作正常 - 所以调试没有帮助。
我想问题出在网站上,而不是 API(如果我错了,请纠正我),所以这里是我的 Startup.cs 和我的 TokenAuthetication-namespace:
启动.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
});
SecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("mysupersecret_secretkey!123"));
services.AddScoped<IDataSerializer<AuthenticationTicket>, TicketSerializer>();
TokenValidationParameters tokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "ExampleIssuer",
ValidateAudience = true,
ValidAudience = "ExampleAudience",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
var serialiser = services.BuildServiceProvider().GetService<IDataSerializer<AuthenticationTicket>>();
var dataProtector = services.BuildServiceProvider()
.GetDataProtector(new[] {"IronSphere.Web.Site-Auth"});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.Cookie.Name = "access_token";
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.ExpireTimeSpan = TimeSpan.FromMinutes(5);
options.ReturnUrlParameter = "returnUrl";
options.TicketDataFormat = new JwtTokenValidator(SecurityAlgorithms.HmacSha256,
tokenValidationParameters, serialiser, dataProtector);
});
services.AddMvc();
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
services.AddTransient(x => x.GetDataProtector(new[] {"IronSphere.Web.Site-Auth"}));
services.AddTransient<IAccountService, AccountService>();
services.AddHttpContextAccessor();
services.AddOptions<AppSettings>();
services.AddMemoryCache();
services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>();
services.AddTransient(typeof(ISession), serviceProvider =>
{
var httpContextAccessor =
serviceProvider.GetService<IHttpContextAccessor>();
return httpContextAccessor.HttpContext.Session;
});
services.AddTransient(typeof(ISession), serviceProvider =>
{
var httpContextAccessor =
serviceProvider.GetService<IHttpContextAccessor>();
return httpContextAccessor.HttpContext.Session;
});
services.AddTransient<IJwtTokenService, JwtTokenService>();
services.AddDataProtection()
.SetApplicationName("KlehnPhotography.Site");
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
app.UseStaticFiles();
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Configuration.GetSection("AppSettings:ImagesPath").Value),
RequestPath = "/images"
});
app.UseFileServer();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
TokenValidator.cs
public interface IJwtTokenService
{
string UnprotectToken(string protectedText);
}
public class JwtTokenService : IJwtTokenService
{
private readonly IDataSerializer<AuthenticationTicket> _ticketSerializer;
private readonly IDataProtector _dataProtector;
public JwtTokenService(IDataSerializer<AuthenticationTicket> serializer, IDataProtector protector)
{
_ticketSerializer = serializer;
_dataProtector = protector;
}
public string UnprotectToken(string protectedText)
{
SecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("mysupersecret_secretkey!123"));
TokenValidationParameters tokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuerSigningKey =
true,
IssuerSigningKey = signingKey,
ValidateIssuer = true,
ValidIssuer = "ExampleIssuer",
ValidateAudience = true,
ValidAudience = "ExampleAudience",
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
AuthenticationTicket authTicket =
_ticketSerializer.Deserialize(_dataProtector.Unprotect(Base64UrlTextEncoder.Decode(protectedText)));
if (authTicket.Properties == null || !authTicket.Properties.Items.Any())
return null;
if (!authTicket.Properties.Items.TryGetValue("jwt", out string embeddedJwt))
throw new ArgumentException("No JWT was found in the Authentication Ticket");
handler.ValidateToken(embeddedJwt, tokenValidationParameters, out SecurityToken validToken);
JwtSecurityToken validJwt;
if ((validJwt = validToken as JwtSecurityToken) == null)
throw new ArgumentException("Invalid JWT");
if (!validJwt.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.Ordinal))
throw new ArgumentException($"Algorithm must be '{SecurityAlgorithms.HmacSha256}'");
return embeddedJwt;
}
}
public class JwtTokenValidator : ISecureDataFormat<AuthenticationTicket>
{
private readonly string _algorithm;
private readonly TokenValidationParameters _validationParameters;
private readonly IDataSerializer<AuthenticationTicket> _ticketSerializer;
private readonly IDataProtector _dataProtector;
public JwtTokenValidator(string algorithm, TokenValidationParameters validationParameters,
IDataSerializer<AuthenticationTicket> ticketSerializer, IDataProtector dataProtector)
{
_algorithm = algorithm;
_validationParameters = validationParameters;
_ticketSerializer = ticketSerializer;
_dataProtector = dataProtector;
}
public AuthenticationTicket Unprotect(string protectedText) => Unprotect(protectedText, null);
public AuthenticationTicket Unprotect(string protectedText, string purpose)
{
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
AuthenticationTicket authTicket;
try
{
authTicket =
_ticketSerializer.Deserialize(_dataProtector.Unprotect(Base64UrlTextEncoder.Decode(protectedText)));
if (authTicket.Properties == null || !authTicket.Properties.Items.Any())
return null;
if (!authTicket.Properties.Items.TryGetValue("jwt", out string embeddedJwt))
throw new ArgumentException("No JWT was found in the Authentication Ticket");
IdentityModelEventSource.ShowPII = true;
handler.ValidateToken(embeddedJwt, this._validationParameters, out SecurityToken validToken);
JwtSecurityToken validJwt;
if ((validJwt = validToken as JwtSecurityToken) == null)
throw new ArgumentException("Invalid JWT");
if (!validJwt.Header.Alg.Equals(_algorithm, StringComparison.Ordinal))
throw new ArgumentException($"Algorithm must be '{_algorithm}'");
}
catch (Exception exception)
{
return null;
}
return authTicket;
}
public string Protect(AuthenticationTicket data) => Protect(data, null);
public string Protect(AuthenticationTicket data, string purpose)
{
byte[] array = _ticketSerializer.Serialize(data);
IDataProtector dataProtector = _dataProtector;
if (!string.IsNullOrEmpty(purpose))
dataProtector = dataProtector.CreateProtector(purpose);
return Base64UrlTextEncoder.Encode(dataProtector.Protect(array));
}
}
知道有什么问题吗?我还可以分享一个 GitHub 存储库。
更新:
API 和网站都在同一台服务器上运行。现在我添加到 Startup for API 中:
services
.AddDataProtection(options => options.ApplicationDiscriminator = "SebastianKlehn")
.PersistKeysToFileSystem(new DirectoryInfo(Configuration.GetSection("AppSettings:KeyStore").Value))
.SetApplicationName("KlehnPhotography.API");
在网站启动中:
services
.AddDataProtection(options => options.ApplicationDiscriminator = "SebastianKlehn")
.PersistKeysToFileSystem(new DirectoryInfo(Configuration.GetSection("AppSettings:KeyStore").Value))
.SetApplicationName("KlehnPhotography.Site");
当我没有设置options.ApplicationDiscriminator
时,没有生成文件
当我启动应用程序时,我在 KeyStore 的目录中得到一个文件。每次删除此文件并重新加载网站时,都会生成一个具有不同 GUID 的新文件。删除 cookie 并登录后,我得到了这个异常:
CryptographicException:在密钥环中找不到密钥 {45030352-111b-460c-8419-94b48817f170}。
所以我猜,网站总是搜索这个(并且总是相同的)GUID:45030352-111b-460c-8419-94b48817f170
即使在 iisreset 之后它也永远不会改变 - 该文件在 KeyStore 文件夹中不存在。
在 API 的日志文件中有:
info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[58]
Creating key {59dc92b2-1932-43d7-bb99-99c2a16e12fb} with creation date 2019-12-25 17:33:17Z, activation date 2019-12-25 17:33:17Z, and expiration date 2020-03-24 17:33:17Z.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {59dc92b2-1932-43d7-bb99-99c2a16e12fb} may be persisted to storage in unencrypted form.
info: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[39]
Writing data to file 'E:\wwwroot\Keys\key-59dc92b2-1932-43d7-bb99-99c2a16e12fb.xml'.
在网站的日志文件中,没有任何关于 DataProtection 或错误或任何内容的信息。只是正常的事情,比如Now listening on...
和Application started
解决方案
CbcAuthenticatedEncryptor
在两种情况下引发此特定异常:您的输入参数乱码或您的 HMAC 验证失败。
您的设置不太可能未能通过第一次检查(据我所知,这只是一个健全性检查),因此它必须是您的有效负载确实已更改(我假设您已经排除)或者验证密码密钥是错误的。
调查KeyRingBasedDataProtector.UnprotectCore
显示您的有效负载是{ (int)magicHeader || (Guid)keyId || (byte[])encryptorSpecificProtectedPayload }
.
在解析之后,keyId
控制IAuthenticatedEncryptor
我们从哪个实例KeyRingProvider
- 在这个阶段必须已经用正确的密钥材料和 IV 初始化。这就是我怀疑您的问题所在 - 因为您Unprotect
在不同的机器上运行 - 您无法访问相同的密钥库并KeyRingProvider
让您成为新的IAuthenticatedEncryptor
(即使您的密钥派生密码相同,我们仍然有IV 可能是随机的——我不会声称知道密钥是如何派生的)。在同一台机器上运行这两个应用程序可能会起作用,因为默认情况下密钥自动发现的工作方式(您似乎依赖它) - 它来自 Windows 注册表或公共文件系统位置(取决于您的操作系统)。
因此,我建议您在服务器之间配置共享密钥存储,如下所示:
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\directory\"));
.SetApplicationName("KlehnPhotography.Site");
看看它是否有帮助。
推荐阅读
- python - 如何使用 Selenium 和 Python 从表中的元素中提取值
- java - 使用 Stream API 和引用方法将集合对象从一种类型转换为另一种类型
- javascript - 如何在 React js 中导出组件以在另一个项目中使用?
- darklang-beta - 如何在 Darklang 中删除个人画布?
- python-3.x - 如何获取数据框中每一行的相关系数
- python - 在python中重新格式化列表
- android - 从另一个小部件 Flutter 更改文本字段的 textSize
- r - 如何映射(匹配)数据框中的列和行?
- python - 如何打印循环用户输入的总和?
- javascript - Konva .to 带有“动态”坐标