如何使用基于 JWT 的身份验证处理文件下载?

IT技术 javascript angularjs jwt
2021-03-15 16:10:23

我正在 Angular 中编写一个 web 应用程序,其中身份验证由 JWT 令牌处理,这意味着每个请求都有一个包含所有必要信息的“身份验证”标头。

这对于 REST 调用非常有效,但我不明白我应该如何处理托管在后端的文件的下载链接(文件驻留在托管 web 服务的同一台服务器上)。

我不能使用常规<a href='...'/>链接,因为它们不会携带任何标头并且身份验证将失败。的各种咒语也一样window.open(...)

我想到的一些解决方案:

  1. 在服务器上生成一个临时的不安全下载链接
  2. 将认证信息作为url参数传递,手动处理case
  3. 通过XHR获取数据并保存文件客户端。

以上都不尽如人意。

1 是我现在使用的解决方案。我不喜欢它有两个原因:首先它在安全方面不是理想的,其次它可以工作但它需要大量的工作,尤其是在服务器上:下载一些东西我需要调用一个服务来生成一个新的“随机" url,将其存储在某处(可能在 DB 上)一段时间,然后将其返回给客户端。客户端获取 url,并使用 window.open 或类似的。当请求时,新的 url 应该检查它是否仍然有效,然后返回数据。

2 似乎至少一样多的工作。

3 似乎有很多工作,即使使用可用的库,还有很多潜在的问题。(我需要提供自己的下载状态栏,将整个文件加载到内存中,然后要求用户将文件保存在本地)。

不过,这项任务似乎是一项非常基本的任务,所以我想知道是否有更简单的东西可以使用。

我不一定要寻找“角度方式”的解决方案。常规的 Javascript 就可以了。

5个回答

这是一种使用下载属性获取 APIURL.createObjectURL在客户端上下载它的方法您将使用 JWT 获取文件,将有效负载转换为 blob,将 blob 放入 objectURL,将锚标记的源设置为该 objectURL,然后在 javascript 中单击该 objectURL。

let anchor = document.createElement("a");
document.body.appendChild(anchor);
let file = 'https://www.example.com/some-file.pdf';

let headers = new Headers();
headers.append('Authorization', 'Bearer MY-TOKEN');

fetch(file, { headers })
    .then(response => response.blob())
    .then(blobby => {
        let objectUrl = window.URL.createObjectURL(blobby);

        anchor.href = objectUrl;
        anchor.download = 'some-file.pdf';
        anchor.click();

        window.URL.revokeObjectURL(objectUrl);
    });

download属性的值将是最终的文件名。如果需要,您可以从内容处置响应标头中挖掘预期的文件名,如其他答案中所述

@Tompi 我也无法在 Edge 和 IE 上完成这项工作
2021-05-07 16:10:23
此解决方案有效,但此解决方案是否可以处理大文件的 UX 问题?如果我有时需要下载一个 300MB 的文件,在单击链接并将其发送到浏览器的下载管理器之前可能需要一些时间来下载。我们可以花精力使用 fetch-progress api 并构建我们自己的下载进度 UI .. 但是,将 300mb 文件加载到 js-land(在内存中?)以将其交给下载的做法也是有问题的经理。
2021-05-09 16:10:23
这对我来说在 chrome 中效果很好。对于 Firefox,它在我将锚点添加到文档后工作: document.body.appendChild(anchor); 没有找到Edge的任何解决方案...
2021-05-10 16:10:23
但是iosSafari对下载属性的支持看起来很红:(
2021-05-13 16:10:23
我一直想知道为什么没有人考虑这个回应。这很简单,因为我们生活在 2017 年,平台支持相当不错。
2021-05-15 16:10:23

技术

根据来自 Auth0 的 Matias Woloski 的建议,JWT 知名布道者,我通过生成带有Hawk的签名请求解决了这个问题

引用沃洛斯基的话:

例如,您解决这个问题的方法是像 AWS 那样生成一个签名请求。

这里有一个用于激活链接的技术示例

后端

我创建了一个 API 来签署我的下载 url:

要求:

POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}

回复:

{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}

使用签名的 URL,我们可以获取文件

要求:

GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c

回复:

Content-Type: multipart/mixed; charset="UTF-8"
Content-Disposition': attachment; filename=protected.file
{BLOB}

前端(由jojoyuji

通过这种方式,您只需单击一次即可完成所有操作:

function clickedOnDownloadButton() {

  postToSignWithAuthorizationHeader({
    url: 'https://path.to/protected.file'
  }).then(function(signed) {
    window.location = signed.url;
  });

}
最后,让这非常不安全的是,URL 是在对任何资源,甚至第三方资源的所有请求的 Referer 标头中发送的。因此,例如,如果您使用 Google Analytics,您将向 Google 发送 URL 令牌并将其全部发送给他们。
2021-05-07 16:10:23
在我为此模式实现的 web api 中,signed.url 仅适用于 1 次访问
2021-05-09 16:10:23
这很酷,但从安全角度来看,我不明白它与 OP 的选项 #2(令牌作为查询字符串参数)有何不同。实际上,我可以想象签名的请求可能更具限制性,即只允许访问特定端点。但是 OP 的 #2 似乎更容易/更少的步骤,这有什么问题?
2021-05-10 16:10:23
此外,带有查询字符串的 URL 将保存在您的用户历史记录中,允许同一台机器的其他用户访问该 URL。
2021-05-12 16:10:23
根据您的 Web 服务器,完整的 URL 可能会记录在其日志文件中。您可能不希望您的 IT 人员访问所有令牌。
2021-05-19 16:10:23

已经提到的现有“fetch/createObjectURL”和“download-token”方法的替代方法是针对新 window的标准表单 POST浏览器读取服务器响应中的附件标头后,将关闭新选项卡并开始下载。同样的方法也恰好适用于在新选项卡中显示 PDF 等资源。

这可以更好地支持旧浏览器并避免必须管理新型令牌。这也将比 URL 上的基本身份验证具有更好的长期支持,因为浏览器正在删除对 url 上的用户名/密码的支持

客户端,target="_blank"即使在失败的情况下,我们也使用避免导航,这对于 SPA(单页应用程序)尤其重要。

主要的警告是服务器端JWT 验证必须从POST 数据不是从 header 中获取令牌如果您的框架使用 Authentication 标头自动管理对路由处理程序的访问,您可能需要将您的处理程序标记为未经身份验证/匿名,以便您可以手动验证 JWT 以确保正确授权。

表单可以动态创建并立即销毁,以便正确清理(注意:这可以在纯 JS 中完成,但为了清晰起见,此处使用 JQuery)-

function DownloadWithJwtViaFormPost(url, id, token) {
    var jwtInput = $('<input type="hidden" name="jwtToken">').val(token);
    var idInput = $('<input type="hidden" name="id">').val(id);
    $('<form method="post" target="_blank"></form>')
                .attr("action", url)
                .append(jwtInput)
                .append(idInput)
                .appendTo('body')
                .submit()
                .remove();
}

只需添加您需要作为隐藏输入提交的任何额外数据,并确保将它们附加到表单中。

如果请求access_token过期download file怎么办?你打算怎么刷新access_token
2021-04-23 16:10:23
我相信这个解决方案被大大低估了。它简单、干净且运行完美。
2021-04-26 16:10:23
@wonsuc 令牌的生命周期与应用程序的其余部分相同。例如,如果您让 access_token 过期,然后尝试访问某些其他资源(例如项目列表),您也会遇到类似的问题。通常人们会使用 refresh_token 定期请求一个新的 access_token,但这取决于您的应用程序,并不特定于这种情况。您可以使用过期信息在发布之前检查 JWT 是否仍然有效,但同样,这是一个完全超出本问题范围的设计选择。
2021-04-29 16:10:23
@IdaAmit 我能理解你的关心。只要 JWT 验证是第一件事,我不确定这比前面提到的任何方法更容易受到 DoS 攻击,所有这些方法都必须验证 JWT 令牌才能下载(或获取下载)令牌)。尽管服务器技术之间存在差异,但通常公共路由是相当轻量级的。只要使用相同的验证代码,开销的差异应该是最小的。仅仅因为框架隐藏了 JWT 验证代码并不意味着它不会有这种开销。
2021-05-18 16:10:23
这个解决方案有效,我唯一关心的是从安全的角度来看。该服务可能会被大量调用附加,尽管它们都具有无效的 jwt 令牌。它使服务很忙。
2021-05-20 16:10:23

我会生成令牌以供下载。

在 angular 中发出经过身份验证的请求以获取临时令牌(例如一个小时),然后将其作为 get 参数添加到 url 中。这样您就可以以您喜欢的任何方式下载文件(window.open ...)

我有一个二进制(静态文件)以这种方式受到保护。我可以在 Webserver 中托管这个静态文件并使用 JWT 访问它吗?在这种情况下,如果用户尝试在没有 JWT 的情况下点击文件 URL 会发生什么?
2021-04-28 16:10:23
@yathirigan 这种方法的缺点是,如果您执行 window.open 或文件的直接链接,则无法将 jwt 标记作为标头传递。
2021-05-10 16:10:23
这是我现在正在使用的解决方案,但我对它并不满意,因为它需要做很多工作,我希望“那里”有更好的解决方案......
2021-05-17 16:10:23
我认为这是可用的最干净的解决方案,我在那里看不到很多工作。但我要么选择较短的令牌有效期(例如 3 分钟),要么通过在服务器上保留令牌列表并删除已使用的令牌(不接受不在我的列表中的令牌)使其成为一次性令牌)。
2021-05-18 16:10:23

詹姆斯回答的纯 JS 版本

function downloadFile (url, token) {
    let form = document.createElement('form')
    form.method = 'post'
    form.target = '_blank'
    form.action = url
    form.innerHTML = '<input type="hidden" name="jwtToken" value="' + token + '">'

    console.log('form:', form)

    document.body.appendChild(form)
    form.submit()
    document.body.removeChild(form)
}