如何在启用JWT Token授权的.NET Core WebApi项目中下载文件 背景 解决方案 总结

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

前几天,做项目的时候遇到一个文件下载的问题。当前系统是一个前后端分离的项目,前端是一个AngularJs项目, 后端是一个.NET Core WebApi项目。后端的Api项目使用了Jwt Token授权,所以每个Api请求都需要传递一个Bearer Token。

这一切都看起来理所当然,但是当需要从WebApi下载文件的时候,出现了问题。以前下载文件的时候,我们可以在Javascript中使用window.open('[文件下载Api]')的方式下载文件,但是这个方法不能接收Bearer Token, 所以就会导致文件下载失败,返回一个401未授权的响应码。

可能有的同学会将这个文件下载Api设置成允许匿名访问,但是这样会导致系统不安全。

那么有什么好一点的方式可以解决这个问题呢?

解决方案

使用Blob对象

Blob对象可以看做是Javascript中的二进制容器, 它可以存储文件的二进制流。所以我们可以通过如下思路完成文件下载:

  1. 创建一个异步请求来下载文件的二进制流,这个请求的头部需要附加Bearer Token,在方法回调中,我们将文件二进制流保存在一个Blob对象中
  2. 我们使用Javascript添加一个虚拟的超链接,超链接的href属性指向了刚刚的Blob对象。
  3. 我们通过模拟点击这个虚拟的超链接,来完成文件下载的功能。
let anchor = document.createElement("a");
let file = 'https://www.example.com/api/getFiles/'+fileId;

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

这个方案有两个缺点:

  1. 就是只有当文件流完全读取到Blob对象中之后,才会触发真正的文件下载。因此如果文件内容过大话,浏览器会有一个长时间的静止,当文件流全部加载到Blob对象之后,才会触发下载操作。所以这里可能需要自己添加一个Loading效果,给用户一些提示。
  2. 并不是所有的浏览器都支持Blob对象,在一些老的浏览器中Blob对象是不被支持的。

使用ASP.NET Core中的Data Protection

在之前的博客中,我有讲解过ASP.NET Core中的Data Protection功能, 我们可以使用Data Protection将一些敏感信息加密。所以这里我们可以将一个需要授权才能使用下载文件的Api, 替换成2个Api

  • 第一个Api是需要授权的,它主要负责查看文件ID是否存在,如果存在,就使用Data Protection, 将这个ID加密,并返回给前端,这个ID的加密时效设置为5秒。

  • 第二个Api是不需要授权的,允许匿名访问。它接收前一个Api提供的加密ID, 如果ID可以解密成功,就返回这个ID对应的文件流。

第一个Api的实例代码:

[HttpGet]
[Route("~/api/file_links/{fileId}")]
public IActionResult GetFileLink(Guid fileId)
{
	if (_files.Any(p => p.FileId == fileId))
	{
		var matchedFile = _files.First(p => p.FileId == fileId);

		return Content(this.protector.Protect(matchedFile.FileId.ToString(),
			TimeSpan.FromSeconds(5)));
	}

	return StatusCode(500);
}

第二个Api的实例代码:

[HttpGet]
[AllowAnonymous]
[Route("~/api/raw_files/{id}")]
public IActionResult GetRawFile(string id)
{
    try
    {
        var rawId = Guid.Parse(this.protector.Unprotect(id));
        var matchedFile = _files.First(p => p.FileId == rawId);
        matchedFile.FileContent.Position = 0;

        return File(matchedFile.FileContent, "text/plain", "helloWorld.txt");
    }
    catch
    {
        return StatusCode(401);
    }
}

使用这种方式,虽然我们开放了一个未经授权就可以访问的Api入口,但是由于使用了Data Protection, 所以对于非法的请求,系统也可以进行一定的屏蔽。

最终效果

针对以上2种下载方式,我创建了一个小项目,项目地址:https://github.com/lamondlu/Sample_DownloadFileInAuth, 打开之后页面如下。

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

普通下载

由于缺少Token, 所以下载失败,返回401

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

使用Blob下载

使用Blob下载之后,文件下载成功

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

使用Data Protection

使用Data Protection后,文件下载成功

如何在启用JWT Token授权的.NET Core WebApi项目中下载文件
背景
解决方案
总结

总结

本文只算抛砖引玉,如果大家有更好的解决方案,欢迎一起讨论。