如何在 Node.js 中生成簽名的 Amazon S3 URL
使用短期簽名 URL 訪問 Amazon S3 存儲桶中的私有內容。
開始使用
為了加快我們的工作,我們將使用 CheatCode Node.js Boilerplate 作為我們工作的起點。首先,讓我們克隆該項目的副本:
終端
git clone https://github.com/cheatcode/nodejs-server-boilerplate.git
接下來,我們需要安裝樣板的依賴:
終端
cd nodejs-server-boilerplate && npm install
在此之後,我們需要安裝 aws-sdk
來自 NPM 的包,這將使我們能夠訪問適用於 Node.js 的 Amazon S3 API:
終端
npm i aws-sdk
最後,啟動開發服務器:
終端
npm run dev
運行之後,我們就可以開始了。
編寫生成簽名 URL 的函數
幸運的是,aws-sdk
作為 S3
的一部分,庫為我們提供了一個簡單的函數 用於生成簽名 URL 的構造函數。我們要做的是編寫一個函數來包裝它並初始化我們與 Amazon S3 的連接。
/lib/getSignedS3URL.js
import AWS from "aws-sdk";
import settings from "./settings";
AWS.config = new AWS.Config({
accessKeyId: settings?.aws?.akid,
secretAccessKey: settings?.aws?.sak,
region: "us-east-1",
signatureVersion: "v4",
});
const s3 = new AWS.S3();
在我們導入 aws-sdk
之後 頂部為 AWS
,我們設置全局AWS.config
值等於 AWS.Config
的新實例 類(注意小寫 cd
之間的細微差別 在我們設置的全局和大寫字母 C
關於構造函數)。
對於該類,我們傳遞一個具有一些不同設置的對象。首先,我們要注意accessKeyId
和 secretAccessKey
特性。這些設置為我們從 AWS 獲得的密鑰,這些密鑰將我們對 S3 的調用與我們的 AWS 賬戶相關聯。
雖然獲取這些密鑰超出了本教程的範圍,但如果您還沒有它們,請閱讀本官方指南,了解如何通過 AWS IAM(身份訪問管理)創建它們。
獲得密鑰後,您可以繼續本教程。
在上面的代碼中,我們不是 將我們的密鑰直接粘貼到我們的代碼中。相反,我們使用 settings
我們正在使用的樣板中內置的功能。它被設置為在每個環境的基礎上為我們的應用加載設置(即,為我們的 development
加載不同的鍵 環境與我們的 production
環境)。
我們這裡導入的文件(位於/lib/settings.js
) 負責決定我們的應用啟動時需要加載哪些設置文件(由 npm run dev
啟動的進程 我們之前運行的命令)。默認情況下,樣板文件包含 settings-development.json
項目根目錄下的文件,該文件旨在包含我們的開發 環境密鑰(將您的密鑰與環境分開可防止不必要的錯誤和安全問題)。
打開該文件,我們要添加您獲得的 AWS 密鑰,如下所示:
/settings-development.json
{
[...]
"aws": {
"akid": "",
"sak": ""
},
[...]
}
在這裡,我們在名為 aws
的文件根目錄處的 JSON 對像中按字母順序添加一個新屬性 (因為我們在 .json
文件,我們需要使用雙引號)。設置到該屬性的是另一個包含我們來自 AWS 的密鑰的對象。這裡,akid
應將其值設置為您的 IAM 用戶和 sak
的訪問密鑰 ID 應該將其值設置為您的秘密訪問密鑰。
/lib/getSignedS3URL.js
import AWS from "aws-sdk";
import settings from "./settings";
AWS.config = new AWS.Config({
accessKeyId: settings?.aws?.akid,
secretAccessKey: settings?.aws?.sak,
region: "us-east-1",
signatureVersion: "v4",
});
const s3 = new AWS.S3();
回到我們的文件,使用 settings
導入,現在我們可以用 settings.aws.akid
指向我們的鍵 和 settings.aws.sak
. ?
在上面的每個屬性之間是一種速記技術,可以幫助我們避免寫出 settings && settings.aws && settings.aws.akid
(settings?.aws?.akid
我們在上面看到的等價於這個)。
設置好鍵後,接下來,我們確保設置 region
我們的 Amazon S3 存儲桶所在的位置。創建 S3 存儲桶也超出了本教程的範圍,因此,如果您尚未設置,請閱讀 AWS 的本指南,然後在完成後繼續本教程。請務必記下您創建存儲桶的區域(如果您找不到該區域的虛線版本,請查看此列表以找到要傳遞給 region
的正確代碼 高於 looks-like-this
)。
接下來,使用您的 region
設置,我們添加 signatureVersion
,將其設置為 v4
(這是AWS簽名協議的最新版本)。
最後,為了完善上面的代碼片段,一旦我們將所有設置傳遞給 AWS.Config
,我們創建一個變量const s3
並將其設置為 AWS.S3()
的新實例 類。
/lib/generateSignedS3URL.js
import AWS from "aws-sdk";
import settings from "./settings";
AWS.config = new AWS.Config({ ... });
const s3 = new AWS.S3();
export default ({ bucket, key, expires }) => {
const signedUrl = s3.getSignedUrl("getObject", {
Key: key,
Bucket: bucket,
Expires: expires || 900, // S3 default is 900 seconds (15 minutes)
});
return signedUrl;
};
就像我們之前暗示的那樣,aws-sdk
庫使生成簽名 URL 變得相當簡單。在這裡,我們添加了一個我們設置為默認 export
的函數 .我們希望該函數將單個參數作為具有三個屬性的 JavaScript 對象接收:
bucket
- 保存我們要為其檢索簽名 URL 的文件(AWS 中的“對象”)的 S3 存儲桶。key
- 我們 S3 存儲桶中文件或“對象”的路徑。expires
- 我們希望 URL 可訪問多長時間(在此持續時間之後,後續嘗試使用該 URL 將失敗)。
在函數內部,我們創建了一個新變量 const signedUrl
我們希望包含我們的 signedUrl
,在這裡,我們期望從調用 s3.getSignedUrl()
得到什麼 . .getSignedUrl()
的獨特之處 這裡的方法是它是 同步的 .這意味著當我們調用該函數時,JavaScript 將等待它返回一個值給我們,然後再評估我們的其餘代碼。
向該函數傳遞兩個參數:我們要執行的 S3 操作(getObject
或 putObject
) 和一個選項對象,該對象描述了我們要為其檢索簽名 URL 的文件。
這裡應該解釋一下操作。這裡,getObject
表示“我們希望為 S3 存儲桶中的現有對象獲取簽名 URL。”如果我們將其更改為 putObject
,我們可以同時創建 一個新對象和 取回它的簽名 URL。如果您總是需要取回已簽名的 URL(而不是在文件已上傳後獲取),這將非常方便。
對於選項對象,在這裡,我們只是從傳遞給包裝函數的參數中復制屬性。您會注意到對像上的屬性傳遞給 .getSignedUrl()
是大寫的,而傳遞給我們的包裝函數的是小寫。在 aws-sdk
, 大寫字母用於傳遞給庫中函數的選項。在這裡,我們為包裝函數使用小寫字母以使事情更簡單。
為了安全起見,對於 Expires
選項,如果我們沒有通過自定義 expires
值到我們的包裝函數中,我們回退到 900
秒或 15 分鐘(這意味著我們從亞馬遜返回的 URL 只能在 15 分鐘內訪問,然後才會失效)。
最後,為了結束我們的函數,我們返回 signedUrl
.接下來,為了測試這一點,我們將設置一個簡單的 Express.js 路由,我們可以在其中調用該函數。
連接 Express 路由以測試 URL 生成
作為本教程使用的 CheatCode Node.js 樣板的一部分,我們提供了一個預先配置的 Express.js 服務器。該服務器是在 /index.js
內部創建的 在項目的根目錄。在那裡,我們創建 Express app
然後——為了保持井井有條——傳遞 app
實例化為一系列函數,我們在其中定義我們的實際路由(或擴展 Express HTTP 服務器)。
/api/index.js
import getSignedS3URL from "../lib/getSignedS3URL";
import graphql from "./graphql/server";
export default (app) => {
graphql(app);
app.use("/s3/signed-url", (req, res) => {
const signedUrl = getSignedS3URL({
bucket: "cheatcode-tutorials",
key: "panda.jpeg",
expires: 5, // NOTE: Make this URL expire in five seconds.
});
res.send(`
<html>
<head>
<title>AWS Signed URL Test</title>
</head>
<body>
<p>URL on Amazon: ${signedUrl}</p>
<img src="${signedUrl}" alt="AWS Signed URL Test" />
<script>
setTimeout(() => {
location = "${signedUrl}";
}, 6 * 1000);
</script>
</body>
</html>
`);
});
};
這裡,在 api()
裡面 從 /index.js
調用的函數 我們剛剛討論的文件,我們採用 Express app
實例作為參數。默認情況下,樣板文件為我們設置了一個 GraphQL 服務器,在這裡,我們將該服務器的創建分離到它自己的函數 graphql()
, 傳入 app
實例,因此可以在內部引用。
接下來,我們在本教程中關心的部分,我們在 /s3/signed-url
處創建一個測試路由 在我們的應用程序中(我們的服務器正在運行,這將在 http://localhost:5001/s3/signed-url
)。在該路由的回調中,我們可以看到對 getSignedS3URL()
的調用 函數(要清楚,我們的包裝函數)。給它,我們通過 bucket
傳遞我們預期的單個選項對象 , key
, 和 expires
.
在這裡,作為演示,我們傳遞 cheatcode-tutorials
bucket(在我們的教程中用於測試),一個已經存在於我們的 bucket panda.jpeg
中的文件 作為 key
, 和 expires
設置為 5
(意思是,使我們返回的 URL 過期並存儲在 const signedUrl
五秒鐘後到這裡)。
我們將這個值設置得相當低,以展示當一個 URL 被訪問超過其過期時間時會發生什麼(您很可能希望根據您的用例將這個值設置得更高)。為了展示這些 URL 的工作原理,我們調用 res.send()
使用一些虛擬 HTML 響應對該路由的任何請求,顯示完整的 signedUrl
我們從亞馬遜回來,因為我們知道這是一個 .jpeg
文件——在 <img />
中呈現該 URL 標記。
在此之下,我們添加了一個帶有 setTimeout()
的短腳本 六秒後將瀏覽器重定向到我們的signedUrl的方法。假設我們的 expires
尊重 5 秒的值,當我們訪問此 URL 時,我們希望它無法訪問:
在我們的演示中,我們可以看到,當我們加載頁面時,我們會返回我們的 URL(以及我們的熊貓圖片)。六秒後,我們重定向到完全相同的 URL(沒有對其進行更改)並發現 AWS 拋出一個錯誤,告訴我們“請求已過期”。這證實了我們簽名的 URL 的行為符合預期,並在創建後 5 秒過期。
總結
在本教程中,我們學習瞭如何使用 aws-sdk
為 S3 對像生成簽名的臨時 URL 包裹。我們學習瞭如何編寫一個包裝函數,它既可以建立與 AWS 的連接,又可以生成我們的簽名 URL。
為了演示我們的功能,最後,我們連接了一個 Express.js 路由,返回一些帶有圖像標籤的 HTML,呈現我們的簽名 URL,然後在幾秒鐘後重定向以驗證簽名 URL 是否正確過期。