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

Posted

技术标签:

【中文标题】uri查询参数中的asp.net核心JWT?【英文标题】:asp.net core JWT in uri query parameter? 【发布时间】:2018-01-27 12:34:28 【问题描述】:

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

这很好用,但是我现在需要能够安全下载文件,所以我不能设置标头 Bearer 值,可以在 URI 中作为 url 参数完成吗?

=-=-=-=-

更新:这是我最终为我的场景所做的,这是一个内部项目,数量非常少,但安全性很重要,未来可能需要扩展:

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

【问题讨论】:

【参考方案1】:

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

不要在页面 URL 中传递不记名令牌:不记名令牌不应该是 传入页面 URL(例如,作为查询字符串参数)。 相反,不记名令牌应该在 HTTP 消息头中传递或 对其采取保密措施的消息体。浏览器, Web 服务器和其他软件可能无法充分保护 浏览器历史记录、Web 服务器日志和其他数据结构。如果承载 令牌在页面 URL 中传递,攻击者可能能够窃取它们 从历史数据、日志或其他不安全的位置。

但是,如果您别无选择或只是不关心安全实践,请参阅Technetium's answer。

【讨论】:

嗯……这在逻辑上是有道理的,尽管我发誓我在标准中读到他们明确提到了 uri 参数来传递令牌;如果我不做某事,我仍然会留下不安全的下载路线。可能是某种 api 调用来获取某种唯一密钥,然后可以将其用于下载路径。 Whups,它不在标准中,它在 jwt.io 介绍页面上:jwt.io/introduction 他们声明:“紧凑:由于它们的尺寸更小,JWT 可以通过URL、POST 参数或 HTTP 标头内。此外,较小的大小意味着传输速度快。”不过你说的有道理。 保护下载的常用方法类似于 Azure 的 Shared Access Signature。查看该链接以大致了解其工作原理,并查看这是否会激发您的用例的解决方案。【参考方案2】:

虽然这有点开箱即用,但我建议您也这样做,因为这是在 .NET 环境中开发时最好的可扩展解决方案。

使用 Azure 存储! 或任何其他类似的在线云存储解决方案。

    它确保您的 Web 应用程序与您的文件分开,因此您不必担心将应用程序移动到不同的 Web 环境。 Web 存储通常比 azure 存储更昂贵(1GB 和大约 3000 次操作(读/写/列表)的总成本约为 0.03 美元。 当您在停机时间更为关键的情况下扩展应用程序时,第 1 点也适用于您使用交换/暂存技术。 Azure 存储负责所谓的Shared Access Tokens (SAS) 的到期

为方便起见,我将在此处仅包含我的代码,因此您不必在 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 只是一个内部枚举,可以轻松跟踪存储某些文件的容器,但这些当然可以是字符串

还有我的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;

所以这将产生像这样的网址: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 是一个公共 url,如果一个强大的 URL 可以在一小时内获得......那么减少过期日期:D

【讨论】:

【参考方案3】:

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

        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();

【讨论】:

【参考方案4】:

这是一个常见的问题。

当您想在单页应用程序的 html 中直接从 API 引用图像或其他文件时,无法在 &lt;img&gt;&lt;a&gt; 元素和请求之间注入 Authorization 请求标头到 API。您可以通过使用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库并在JwtBearerOptions上调用AddQueryStringAuthentication()扩展方法。或者,如果您想手动操作,您当然也可以这样做。这是一个代码示例,它显示了作为 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;
                    
                ;
            
        );

【讨论】:

尽管挥舞着手指,但这样做是有正当理由的。例如,它实际上是保护 Google Cloud PubSub 的“推送”实现的端点的推荐方法。 cloud.google.com/pubsub/docs/faq#security 太棒了!不要忘记在 Configure 方法中添加 UseAuthentication(就像我一开始所做的那样)。您还可以使用事件 OnTokenValidated 添加自定义声明,有关 Open Id Connect 的示例,请参阅 joonasw.net/view/adding-custom-claims-aspnet-core-2。 如果没有在 URL 上指定,它可以与标头中的令牌一起使用吗?在我的情况下,我在到达 OnMessageReceive 事件之前被拒绝 很好的解决方案,谢谢。代码示例缺少app.UseJwtBearerQueryString() 语句。【参考方案5】:

如果你还需要它,你必须在localStorage上设置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'()))

【讨论】:

以上是关于uri查询参数中的asp.net核心JWT?的主要内容,如果未能解决你的问题,请参考以下文章

具有固定 URI 的 ASP.NET 路由不映射查询字符串参数

控制器 ASP.net 核心 2.1 中的 Jwt 角色身份验证

asp.net 核心中的 JWT 身份验证验证

JWT:如何从声明中的特定键获取值列表。 C# Asp.Net 核心

通过 ASP.NET 核心身份中的角色声明进行 JWT 身份验证

签名证书 jwt asp.net 核心中的密钥长度应该是多少