asp.net uri 查询参数中的核心 JWT?

asp.net core JWT in uri query parameter?

我有一个受 JWT 和 Authorize 属性保护的 api,在客户端我使用 jquery ajax 调用来处理它。

这很好用,但是我现在需要能够安全地下载文件,所以我无法设置 header 承载值,它可以在 URI 中作为 url 完成吗参数?

=-=-=-=-

更新:这就是我最终为我的场景所做的,这是一个 in-house 项目并且数量非常少,但安全性很重要,将来可能需要扩展:

当用户登录时,我生成一个随机下载密钥并将其连同他们的 JWT 的到期日期和 return 客户端的下载密钥一起放入数据库中的用户记录中。如果存在具有下载密钥的查询参数并且该密钥存在于用户记录中并且该到期日期尚未过去,则下载路由被保护为仅允许下载。这样每个用户的 dl 密钥都是唯一的,只要用户的身份验证 session 有效并且可以轻松撤销。

尽管在 URL 中包含 JWT 在技术上是可行的,但强烈 不鼓励这样做。请参阅 here 中的引述,这解释了为什么这是一个坏主意:

Don't pass bearer tokens in page URLs: Bearer tokens SHOULD NOT be passed in page URLs (for example, as query string parameters). Instead, bearer tokens SHOULD be passed in HTTP message headers or message bodies for which confidentiality measures are taken. Browsers, web servers, and other software may not adequately secure URLs in the browser history, web server logs, and other data structures. If bearer tokens are passed in page URLs, attackers might be able to steal them from the history data, logs, or other unsecured locations.

但是,如果您别无选择或根本不关心安全措施,请参阅

如果你仍然需要它,你必须在 localStorage.After 上设置 jwt 令牌,你必须使用以下代码创建一个新的 header:

'functionName'():Headers{
        let header =new Headers();
        let token = localStorage.getItem('token')
        header.append('Authorization',`Bearer ${token}`);

        return header;
    }

将 Hader 添加到 http 请求。

return this.http.get('url',new RequestOptions({headers:this.'serviceName'.'functionName'()}))

这是一个常见问题。

每当您想在单页应用程序 HTML 中直接从 API 引用图像或其他文件时,无法注入 Authorization 请求 [= <img><a> 元素与对 API 的请求之间的 37=]。您可以通过使用一些相当新的浏览器功能来避免这种情况,如 here 所述,但您可能需要支持缺少此功能的浏览器。

幸运的是,RFC 6750 specifies a way to do exactly what you're asking via 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:

Because of the security weaknesses associated with the URI method (see Section 5), including the high likelihood that the URL containing the access token will be logged, it SHOULD NOT be used unless it is impossible to transport the access token in the "Authorization" request header field or the HTTP request entity-body.

如果您仍然决定实施“URI 查询参数”身份验证,则可以使用 Invio.Extensions.Authentication.JwtBearer library and call AddQueryStringAuthentication() extension method on JwtBearerOptions. Or, if you want to do it manually, you can certainly do that as well. Here's a code sample that shows both ways as extensions of the 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;
                    }
                };
            }
        );
}

您可以使用中间件从查询参数设置授权header:

        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 存储! 或任何其他类似的在线云存储解决方案。

  1. 它确保您的 Web 应用程序与您的文件分开,因此您不必担心将应用程序移动到不同的 Web 环境。
  2. Web 存储大多比 Azure 存储更贵(1GB,大约 3000 次操作 (read/write/list),总成本约为 0.03 美元。
  3. 当您在停机时间更为严重的地方扩展应用程序时,当您使用 swapping/staging 技术时,第 1 点也适用。
  4. Azure 存储负责所谓的 Shared Access Tokens (SAS)
  5. 的到期

为了简单起见,我将只在此处包含我的代码,这样您就不必 google 其余的

所以我在我的案例中所做的是,我的所有文件都在数据库中保存为 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 只是一个内部枚举,用于轻松跟踪存储某些文件的容器,但这些当然可以是字符串

还有我的AzureStorageclass:

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;
}

所以这将产生 url 像这样:http://127.0.0.1:10000/devstoreaccount1/invoices/NL3_2002%20-%202019-04-12.pdf?sv=2018-03-28&sr=b&sig=gSiohA%2BGwHj09S45j2Deh%2B1UYP1RW1Fx5VGeseNZmek%3D&st=2019-04-18T14%3A16%3A55Z&se=2019-04-18T15%3A46%3A55Z&sp=r

当然,无论是否允许用户查看文件,您都必须在检索附件时应用自己的身份验证逻辑。但这都可以通过 JWT 令牌在控制器或存储库中完成。我不会担心 URL 是 public url,如果一个人如此强大地得到 URL... 在一小时内... 那么减少过期日期:D