【问题标题】:How to handle file downloads with JWT based authentication?如何使用基于 JWT 的身份验证处理文件下载?
【发布时间】:2015-06-09 17:09:26
【问题描述】:

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

这非常适合 REST 调用,但我不明白我应该如何处理托管在后端的文件的下载链接(这些文件位于托管 Web 服务的同一台服务器上)。

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

我想到的一些解决方案:

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

以上所有都不尽如人意。

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

2 似乎至少有同样多的工作量。

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

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

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

【问题讨论】:

  • 远程是指可下载文件与 Angular 应用程序位于不同的域中吗?您是否控制遥控器(有权修改其后端)?
  • 我的意思是文件数据不在客户端(浏览器)上;该文件托管在同一个域上,我可以控制后端。我将更新问题以减少歧义。
  • 选项2的难度取决于您的后端。如果您可以告诉后端在通过身份验证层时检查查询字符串以及 JWT 的授权标头,那么您就完成了。您使用的是哪个后端?

标签: javascript angularjs jwt


【解决方案1】:

我会生成令牌以供下载。

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

【讨论】:

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

技术

基于来自 Auth0 的 Matias Woloski 的 this advice,已知的 JWT 布道者,我通过使用 Hawk 生成签名请求来解决它。

引用沃洛斯基的话:

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

Here you have an example此技术,用于激活链接。

后端

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

请求:

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

}

【讨论】:

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

这是一种使用the download attributethe fetch 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 属性的值将是最终的文件名。如果需要,您可以从内容处置响应标头as described in other answers 中挖掘出预期的文件名。

【讨论】:

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

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

这对旧浏览器有更好的支持,并且避免了管理新类型的令牌。这也将比 URL 上的基本身份验证具有更好的长期支持,因为 support for username/password on the url is being removed by browsers

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

主要的警告是 服务器端 JWT 验证必须从 POST 数据 中获取令牌,而不是从标头。如果您的框架使用 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();
}

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

【讨论】:

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

詹姆斯回答的纯 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)
}

【讨论】:

    猜你喜欢
    • 2020-09-19
    • 2018-10-21
    • 2016-04-03
    • 1970-01-01
    • 1970-01-01
    • 2017-05-29
    • 2018-12-11
    • 1970-01-01
    相关资源
    最近更新 更多