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

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

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

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

我想到了一些解决办法:

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

以上所有情况都不能令人满意。

1是我现在正在使用的解决方案。我不喜欢它有两个原因: 第一,它的安全性不理想,第二,它的工作,但它需要相当多的工作,特别是在服务器上: 下载一些东西,我需要调用一个服务,生成一个新的“随机”网址,存储在某个地方(可能在数据库)一段时间,并返回给客户端。客户端获取 url,并使用 window.open 或与之类似的命令。当被请求时,新的 URL 应该检查它是否仍然有效,然后返回数据。

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

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

这个任务看起来很简单,所以我想知道是否有更简单的方法可以使用。

我不一定要寻找一个解决方案“角度的方式”。

53915 次浏览

我将生成用于下载的令牌。

在 angle 中,通过身份验证请求获取一个临时令牌(比如一个小时) ,然后将其作为 get 参数添加到 url 中。这样,您就可以按照自己喜欢的方式下载文件(window.open...)

技巧

基于 Auth0的 Matias Woloski 的 这个建议,我通过使用 霍克生成一个签名请求来解决这个问题。

引用沃洛斯基的话:

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

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

后端

我创建了一个 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;
});


}

下面是一种使用 Download 属性提取 APICreateObjectURL在客户机上下载它的方法。您可以使用 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属性的值将是最终的文件名。如果需要,可以从内容处置响应头 正如其他答案所描述的中挖掘预期的文件名。

对于已经提到的现有“获取/创建对象 URL”和“下载令牌”方法,另一种替代方法是标准的 以新窗口为目标的 POST 格式。一旦浏览器读取服务器响应上的附件头,它将关闭新的选项卡并开始下载。同样的方法也适用于在新选项卡中显示类似 PDF 的资源。

这对老式浏览器有更好的支持,并且可以避免管理新类型的令牌。这也将有更好的长期支持比基本认证的 URL,因为 浏览器正在删除对网址上用户名/密码的支持

客户端上,我们使用 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();
}

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

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