我有一个受JWT和Authorize属性保护的api,在客户端我使用jquery ajax调用来处理它。
这工作正常,但我现在需要能够安全下载文件,所以我不能设置标头承载值,它可以作为url参数在URI中完成吗?
=-=-=-=-
更新:这是我最终为我的场景做的,这是一个内部项目,而且数量非常少但安全性很重要,未来可能需要扩展:
当用户登录时,我生成一个随机下载密钥并将其放入数据库中的用户记录以及其JWT的到期日期,并将下载密钥返回给客户端。如果存在具有下载密钥且该密钥存在于用户记录中并且未到期的查询参数,则下载路由受到保护以仅允许下载。这样,dl密钥对于每个用户是唯一的,只要用户的auth会话有效并且可以容易地撤销,则该密钥有效。
虽然在技术上可以在URL中包含JWT,但强烈建议不要这样做。请参阅here的引用,这解释了为什么这是一个坏主意:
不要在页面URL中传递承载令牌:不应在页面URL中传递承载令牌(例如,作为查询字符串参数)。相反,承载令牌应该在HTTP消息头或消息体中传递,对其采取保密措施。浏览器,Web服务器和其他软件可能无法充分保护浏览器历史记录,Web服务器日志和其他数据结构中的URL。如果在页面URL中传递了承载令牌,则攻击者可能能够从历史数据,日志或其他不安全的位置窃取它们。
这是一个常见问题。
无论何时您想在单页面应用程序的HTML中直接从API引用图像或其他文件,都无法在Authorization
或<img>
元素和API请求之间注入<a>
请求标头。您可以通过使用here所述的一些相当新的浏览器功能来回避这一点,但您可能需要支持缺少此功能的浏览器。
幸运的是,RFC 6750指定了一种方法,可以通过the "URI Query Parameter" authentication approach完全按照您的要求进行操作。如果遵循其约定,您将使用以下格式接受JWT:
https://server.example.com/resource?access_token=mF_9.B5f-4.1JqM&p=q
正如另一个answer和RFC 6750本身所述,你应该只在必要时这样做。来自RFC:
由于与URI方法相关的安全漏洞(请参阅Section 5),包括记录包含访问令牌的URL的可能性很高,除非无法在“授权”请求中传输访问令牌,否则不应使用它。标头字段或HTTP请求实体。
如果您仍然决定实施“URI查询参数”身份验证,则可以使用Invio.Extensions.Authentication.JwtBearer
库并在AddQueryStringAuthentication()
上调用JwtBearerOptions
扩展方法。或者,如果你想手动完成,你当然也可以这样做。这是一个代码示例,它显示了两种方式作为Microsoft.AspNetCore.Authentication.JwtBearer
库的扩展。
public void ConfigureServices(IServiceCollection services) {
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
options => {
var authentication = this.configuration.GetSection("Authentication");
options.TokenValidationParameters = new TokenValidationParameters {
ValidIssuers = authentication["Issuer"],
ValidAudience = authentication["ClientId"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(authentication["ClientSecret"])
)
};
// OPTION 1: use `Invio.Extensions.Authentication.JwtBearer`
options.AddQueryStringAuthentication();
// OPTION 2: do it manually
options.Events = new JwtBearerEvents {
OnMessageReceived = (context) => {
StringValues values;
if (!context.Request.Query.TryGetValue("access_token", out values)) {
return Task.CompletedTask;
}
if (values.Count > 1) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"Only one 'access_token' query string parameter can be defined. " +
$"However, {values.Count:N0} were included in the request."
);
return Task.CompletedTask;
}
var token = values.Single();
if (String.IsNullOrWhiteSpace(token)) {
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
context.Fail(
"The 'access_token' query string parameter was defined, " +
"but a value to represent the token was not included."
);
return Task.CompletedTask;
}
context.Token = token;
return Task.CompletedTask;
}
};
}
);
}
如果您仍然需要它,则必须在localStorage上设置jwt标记。之后,您必须使用以下代码创建新标头:
'functionName'():Headers{
let header =new Headers();
let token = localStorage.getItem('token')
header.append('Authorization',`Bearer ${token}`);
return header;
}
将标头添加到http请求。
return this.http.get('url',new RequestOptions({headers:this.'serviceName'.'functionName'()}))
您可以使用中间件从查询参数设置授权标头:
public class SecureDownloadUrlsMiddleware
{
private readonly RequestDelegate next;
public SecureDownloadUrlsMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
// get the token from query param
var token = context.Request.Query["t"];
// set the authorization header only if it is empty
if (string.IsNullOrEmpty(context.Request.Headers["Authorization"]) &&
!string.IsNullOrEmpty(token))
{
context.Request.Headers["Authorization"] = $"Bearer {token}";
}
await next(context);
}
}
然后在Startup.cs中使用认证中间件之前的中间件:
app.UseMiddleware(typeof(SecureDownloadUrlsMiddleware));
app.UseAuthentication();
虽然这有点开箱即用,但我建议您这样做,因为这是在.NET环境中开发时最好的可扩展解决方案。
使用Azure存储!或任何其他类似的在线云存储解决方案。
为了您的简单,我将在此处包含我的代码,这样您就不必谷歌了
所以我在我的情况下,我的所有文件都保存为数据库中的Attachments
(当然不是实际的文件)。
当有人请求附件时,我会快速检查过期日期是否已过,如果是,我们应该生成新的URL。
//where ever you want this to happen, in the controller before going to the client for example
private async Task CheckSasExpire(IEnumerable<AttachmentModel> attachments)
{
foreach (AttachmentModel attachment in attachments)
{
await CheckSasExpire(attachment);
}
}
private async Task CheckSasExpire(AttachmentModel attachment)
{
if (attachment != null && attachment.LinkExpireDate < DateTimeOffset.UtcNow && !string.IsNullOrWhiteSpace(attachment.AzureContainer))
{
Enum.TryParse(attachment.AzureContainer, out AzureStorage.ContainerEnum container);
string url = await _azureStorage.GetFileSasLocator(attachment.Filename, container);
attachment.FileUrl = url;
attachment.LinkExpireDate = DateTimeOffset.UtcNow.AddHours(1);
await _attachmentRepository.UpdateAsync(attachment.AttachmentId, attachment);
}
}
AzureStorage.ContainerEnum
只是一个内部枚举,可以轻松跟踪容器中存储的某些文件,但这些可以是字符串当然
而我的AzureStorage
课程:
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
public async Task<string> GetFileSasLocator(string filename, ContainerEnum container, DateTimeOffset expire = default(DateTimeOffset))
{
var cont = await GetContainer(container);
CloudBlockBlob blockBlob = cont.GetBlockBlobReference(filename);
DateTimeOffset expireDate = DateTimeOffset.UtcNow.AddHours(1);//default
if (expire != default(DateTimeOffset) && expire > expireDate)
{
expireDate = expire.ToUniversalTime();
}
SharedAccessBlobPermissions permission = SharedAccessBlobPermissions.Read;
var sasConstraints = new SharedAccessBlobPolicy
{
SharedAccessStartTime = DateTime.UtcNow.AddMinutes(-30),
SharedAccessExpiryTime = expireDate,
Permissions = permission
};
var sasToken = blockBlob.GetSharedAccessSignature(sasConstraints);
return blockBlob.Uri + sasToken;
}
private async Task<CloudBlobContainer> GetContainer(ContainerEnum container)
{
//CloudConfigurationManager.GetSetting("StorageConnectionString")
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_config["StorageConnectionString"]);
CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
string containerName = container.ToString().ToLower();
CloudBlobContainer cloudContainer = blobClient.GetContainerReference(containerName);
await cloudContainer.CreateIfNotExistsAsync();
return cloudContainer;
}
当然,如果允许用户查看文件,则必须在检索附件时应用自己的身份验证逻辑。但这可以通过JWT令牌以及控制器或存储库来完成。我不担心URL是公共网址,如果一个人如此强大,以便在一小时内获得该网址......那么减少过期日期:D