背景
前幾天,做項目的時候遇到一個文件下載的問題。當前系統是一個前后端分離的項目,前端是一個AngularJs項目, 后端是一個.NET Core WebApi項目。后端的Api項目使用了Jwt Token授權,所以每個Api請求都需要傳遞一個Bearer Token。
這一切都看起來理所當然,但是當需要從WebApi下載文件的時候,出現了問題。以前下載文件的時候,我們可以在Javascript中使用window.open('[文件下載Api]')
的方式下載文件,但是這個方法不能接收Bearer Token, 所以就會導致文件下載失敗,返回一個401未授權的響應碼。
可能有的同學會將這個文件下載Api設置成允許匿名訪問,但是這樣會導致系統不安全。
那么有什么好一點的方式可以解決這個問題呢?
解決方案
使用Blob對象
Blob對象可以看做是Javascript中的二進制容器, 它可以存儲文件的二進制流。所以我們可以通過如下思路完成文件下載:
- 創建一個異步請求來下載文件的二進制流,這個請求的頭部需要附加Bearer Token,在方法回調中,我們將文件二進制流保存在一個Blob對象中
- 我們使用Javascript添加一個虛擬的超鏈接,超鏈接的href屬性指向了剛剛的Blob對象。
- 我們通過模擬點擊這個虛擬的超鏈接,來完成文件下載的功能。
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);
});
這個方案有兩個缺點:
- 就是只有當文件流完全讀取到Blob對象中之后,才會觸發真正的文件下載。因此如果文件內容過大話,瀏覽器會有一個長時間的靜止,當文件流全部加載到Blob對象之后,才會觸發下載操作。所以這里可能需要自己添加一個Loading效果,給用戶一些提示。
- 並不是所有的瀏覽器都支持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, 打開之后頁面如下。
普通下載
由於缺少Token, 所以下載失敗,返回401
使用Blob下載
使用Blob下載之后,文件下載成功
使用Data Protection
使用Data Protection后,文件下載成功
總結
本文只算拋磚引玉,如果大家有更好的解決方案,歡迎一起討論。