Javascript 如何使用基于 JWT 的身份验证处理文件下载?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/29452031/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to handle file downloads with JWT based authentication?
提问by Marco Righele
I'm writing a webapp in Angular where authentication is handled by a JWT token, meaning that every request has an "Authentication" header with all the necessary information.
我正在 Angular 中编写一个 web 应用程序,其中身份验证由 JWT 令牌处理,这意味着每个请求都有一个包含所有必要信息的“身份验证”标头。
This works nicely for REST calls, but I don't understand how I should handle download links for files hosted on the backend (the files reside on the same server where the webservices are hosted).
这对于 REST 调用非常有效,但我不明白我应该如何处理托管在后端的文件的下载链接(文件驻留在托管 web 服务的同一台服务器上)。
I can't use regular <a href='...'/>links since they won't carry any header and the authentication will fail. Same for the various incantations of window.open(...).
我不能使用常规<a href='...'/>链接,因为它们不会携带任何标头并且身份验证将失败。的各种咒语也一样window.open(...)。
Some solutions I thought of:
我想到的一些解决方案:
- Generate a temporary unsecured download link on the server
- Pass the authentication information as an url parameter and manually handle the case
- Get the data through XHR and save the file client side.
- 在服务器上生成一个临时的不安全下载链接
- 将认证信息作为url参数传递,手动处理case
- 通过XHR获取数据并保存文件客户端。
All of the above are less than satisfactory.
以上都不尽如人意。
1 is the solution I am using right now. I don't like it for two reasons: first it is not ideal security-wise, second it works but it requires quite a lot of work especially on the server: to download something I need to call a service that generates a new "random" url, stores it somewhere (possibly on the DB) for a some time, and returns it to the client. The client gets the url, and use window.open or similar with it. When requested, the new url should check if it is still valid, and then return the data.
1 是我现在使用的解决方案。我不喜欢它有两个原因:首先它在安全方面不是理想的,其次它可以工作但它需要大量的工作,尤其是在服务器上:下载一些东西我需要调用一个服务来生成一个新的“随机" url,将其存储在某处(可能在 DB 上)一段时间,然后将其返回给客户端。客户端获取 url,并使用 window.open 或类似的。当请求时,新的 url 应该检查它是否仍然有效,然后返回数据。
2 seems at least as much work.
2 似乎至少一样多的工作。
3 seems a lot of work, even using available libraries, and lot of potential issues. (I would need to provide my own download status bar, load the whole file in memory and then ask the user to save the file locally).
3 似乎有很多工作,即使使用可用的库,还有很多潜在的问题。(我需要提供自己的下载状态栏,将整个文件加载到内存中,然后要求用户将文件保存在本地)。
The task seems a pretty basic one though, so I'm wondering if there is anything much simpler that I can use.
不过,这项任务似乎是一项非常基本的任务,所以我想知道是否有更简单的东西可以使用。
I'm not necessarily looking for a solution "the Angular way". Regular Javascript would be fine.
我不一定要寻找“角度方式”的解决方案。常规的 Javascript 就可以了。
回答by Technetium
Here's a way to download it on the client using the download attribute, the fetch API, and URL.createObjectURL. You would fetch the file using your JWT, convert the payload into a blob, put the blob into an objectURL, set the source of an anchor tag to that objectURL, and click that objectURL in javascript.
这是一种使用下载属性、获取 API和URL.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);
});
The value of the downloadattribute will be the eventual file name. If desired, you can mine an intended filename out of the content disposition response header as described in other answers.
该download属性的值将是最终的文件名。如果需要,您可以从内容处置响应标头中挖掘预期的文件名,如其他答案中所述。
回答by Ezequias Dinella
Technique
技术
Based on this adviceof Matias Woloski from Auth0, known JWT evangelist, I solved it by generating a signed request with Hawk.
根据来自 Auth0 的 Matias Woloski 的建议,已知 JWT 布道者,我通过生成带有Hawk的签名请求解决了这个问题。
Quoting Woloski:
引用沃洛斯基的话:
The way you solve this is by generating a signed request like AWS does, for example.
例如,您解决这个问题的方法是像 AWS 那样生成一个签名请求。
Here you have an exampleof this technique, used for activation links.
backend
后端
I created an API to sign my download urls:
我创建了一个 API 来签署我的下载网址:
Request:
要求:
POST /api/sign
Content-Type: application/json
Authorization: Bearer...
{"url": "https://path.to/protected.file"}
Response:
回复:
{"url": "https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c"}
With a signed URL, we can get the file
使用签名的 URL,我们可以获取文件
Request:
要求:
GET https://path.to/protected.file?bewit=NTUzMDYzZTQ2NDYxNzQwMGFlMDMwMDAwXDE0NTU2MzU5OThcZDBIeEplRHJLVVFRWTY0OWFFZUVEaGpMOWJlVTk2czA0cmN6UU4zZndTOD1c
Response:
回复:
Content-Type: multipart/mixed; charset="UTF-8"
Content-Disposition': attachment; filename=protected.file
{BLOB}
frontend (by jojoyuji)
前端(由jojoyuji 提供)
This way you can do it all on a single user click:
通过这种方式,您只需单击一次即可完成所有操作:
function clickedOnDownloadButton() {
postToSignWithAuthorizationHeader({
url: 'https://path.to/protected.file'
}).then(function(signed) {
window.location = signed.url;
});
}
回答by James
An alternative to the existing "fetch/createObjectURL" and "download-token" approaches already mentioned is a standard Form POST that targets a new window. Once the browser reads the attachment header on the server response, it will close the new tab and begin the download. This same approach also happens to work nicely for displaying a resource like a PDF in a new tab.
已经提到的现有“fetch/createObjectURL”和“download-token”方法的替代方法是针对新 window的标准表单 POST。浏览器读取服务器响应中的附件标头后,将关闭新选项卡并开始下载。同样的方法也恰好适用于在新选项卡中显示 PDF 等资源。
This has better support for older browsers and avoids having to manage a new type of token. This will also have better long-term support than basic auth on the URL, since support for username/password on the url is being removed by browsers.
这可以更好地支持旧浏览器并避免必须管理新型令牌。这也将比 URL 上的基本身份验证具有更好的长期支持,因为浏览器正在删除对 url 上的用户名/密码的支持。
On the client-sidewe use target="_blank"to avoid navigation even in failure cases, which is particularly important for SPAs (single page apps).
在客户端,target="_blank"即使在失败的情况下,我们也使用避免导航,这对于 SPA(单页应用程序)尤其重要。
The major caveat is that the server-sideJWT validation has to get the token from the POST dataand not from the header. If your framework manages access to route handlers automatically using the Authentication header, you may need to mark your handler as unauthenticated/anonymous so that you can manually validate the JWT to ensure proper authorization.
主要的警告是服务器端JWT 验证必须从POST 数据而不是从 header 中获取令牌。如果您的框架使用 Authentication 标头自动管理对路由处理程序的访问,您可能需要将您的处理程序标记为未经身份验证/匿名,以便您可以手动验证 JWT 以确保正确授权。
The form can be dynamically created and immediately destroyed so that it is properly cleaned up (note: this can be done in plain JS, but JQuery is used here for clarity) -
表单可以动态创建并立即销毁,以便正确清理(注意:这可以在纯 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();
}
Just add any extra data you need to submit as hidden inputs and make sure they are appended to the form.
只需添加您需要作为隐藏输入提交的任何额外数据,并确保将它们附加到表单中。
回答by Fred
I would generate tokens for download.
我会生成令牌以供下载。
Within angular make an authenticated request to obtain a temporary token (say an hour) then add it to the url as a get parameter. This way you can download files in any way you like (window.open ...)
在 angular 中发出经过身份验证的请求以获取临时令牌(例如一个小时),然后将其作为 get 参数添加到 url 中。这样您就可以以您喜欢的任何方式下载文件(window.open ...)
回答by AlbinoDrought
An additional solution: using basic authentication. Although it requires a bit of work on the backend, tokens won't be visible in logs and no URL signing will have to be implemented.
另一个解决方案:使用基本身份验证。虽然它需要在后端做一些工作,但令牌在日志中是不可见的,并且不需要实施 URL 签名。
Client Side
客户端
An example URL could be:
一个示例 URL 可以是:
http://jwt:<user jwt token>@some.url/file/35/download
http://jwt:<user jwt token>@some.url/file/35/download
Example with dummy token:
带有虚拟令牌的示例:
http://jwt:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwIiwibmFtZSI6IiIsImlhdCI6MH0.KsKmQOZM-jcy4l_7NFsv1lWfpH8ofniVCv75ZRQrWno@some.url/file/35/download
http://jwt:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIwIiwibmFtZSI6IiIsImlhdCI6MH0.KsKmQOZM-jcy4l_7NFsv1lWfpH8ofniVCv75ZRQrWno@some.url/file/35/download
You can then shove this in <a href="...">or window.open("...")- the browser handles the rest.
然后您可以将其推入<a href="...">或window.open("...")- 浏览器处理其余部分。
Server Side
服务器端
Implementation here is up to you, and is dependent on your server setup - it's not too much different from using the ?token=query parameter.
此处的实现取决于您,并且取决于您的服务器设置 - 这与使用?token=查询参数没有太大区别。
Using Laravel, I went the easy route and transformed the basic authentication password into the JWT Authorization: Bearer <...>header, letting the normal authentication middleware handle the rest:
使用 Laravel,我采用了简单的方法,将基本身份验证密码转换为 JWTAuthorization: Bearer <...>标头,让普通身份验证中间件处理其余部分:
class CarryBasic
{
/**
* @param Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, \Closure $next)
{
// if no basic auth is passed,
// or the user is not "jwt",
// send a 401 and trigger the basic auth dialog
if ($request->getUser() !== 'jwt') {
return $this->failedBasicResponse();
}
// if there _is_ basic auth passed,
// and the user is JWT,
// shove the password into the "Authorization: Bearer <...>"
// header and let the other middleware
// handle it.
$request->headers->set(
'Authorization',
'Bearer ' . $request->getPassword()
);
return $next($request);
}
/**
* Get the response for basic authentication.
*
* @return void
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*/
protected function failedBasicResponse()
{
throw new UnauthorizedHttpException('Basic', 'Invalid credentials.');
}
}

