在不斷發展的 Web 開發領域,無服務器應用程序正在穩步留下自己的印記。 它們無可否認的優勢,例如無與倫比的可擴展性和強大的性能,使它們脫穎而出。 無服務器的美妙之處在於它的承諾:提供強大的解決方案,不僅可以輕鬆擴展,而且預算友好。
您是否曾經夢想過推出一個商業創意,但卻被創建用戶註冊和登錄系統的技術細節所阻礙? 雖然 Netlify 等平台和各種基於雲的數據庫確實存在,但它們無法與 Cloudflare 提供的功能相比。 許多替代方案在擴展時可能會增加成本,但對於 Cloudflare Pages,情況有所不同。
在過去的幾個月裡,我一直斷斷續續地致力於這個項目和演示,同時兼顧我的其他承諾。 我對等待表示歉意,尤其是對那些熱切期待 這個系統的人。
釋放 Cloudflare 頁面的力量
- 無縫可擴展性:輕鬆管理無限的用戶註冊。
- 成本效益:告別意外的管理費用; 享受一致的定價。
- 極速:體驗前所未有的性能。
- 可靠的安全性:您的用戶數據仍然受到保護且安全。
我們將使用什麼
- Cloudflare 頁面
- Cloudflare 頁面功能
- Cloudflare Workers KV
什麼是 Cloudflare 頁面?
Cloudflare Pages 是一個現代、用戶友好的平台,供開發人員構建、部署和託管其網站。 它提供與 GitHub 的無縫集成,這意味著您只需將代碼推送到 GitHub,Cloudflare Pages 將處理其餘的工作 - 構建、部署,甚至更新。
它的工作原理如下:
- 集成工作流程:Cloudflare Pages 圍繞 git 工作流程構建。 將 GitHub 存儲庫連接到 Cloudflare Pages 後,每次您推送到所選分支時,它都會開始構建和部署您的站點。
- JAMstack 優化:Cloudflare Pages 支持 JAMstack 原則,這意味著您可以使用您喜歡的靜態站點生成器或 JavaScript 框架(包括但不限於 Jekyll、Hugo、Next.js 和 React)構建站點。
- 快速、安全的交付:Pages 由全球分佈的 Cloudflare 網絡提供支持,確保您的網站可用且快速,無論您的受眾身在何處。 此外,Cloudflare 固有的安全功能可保護您的網站免受威脅。
- 持續部署:每次您在 GitHub 存儲庫上進行更新時,Cloudflare Pages 都會自動構建和部署您的網站。 這使您可以快速迭代並使部署過程變得輕而易舉。
- 自定義域和 HTTPS:使用 Pages,您可以將自定義域連接到您的站點,並且它在所有站點上提供免費、自動的 HTTPS,以確保連接始終安全。
- 預覽部署:每當您在鏈接的 GitHub 存儲庫中創建新的拉取請求時,Cloudflare Pages 都會自動生成一個唯一的預覽 URL,以便您在上線之前查看更改。
無論您是獨立開發者還是大型團隊的一員,Cloudflare Pages 都提供了一種簡單、快速且安全的方式讓您的網站上線。
鑑於上述情況,對於這個用戶註冊系統,我選擇了純粹且直接的 HTML 頁面,避開任何額外的框架或構建工具。 這種方法確保了無與倫比的簡單性,並提供了實現任何期望結果的靈活性。
什麼是 Cloudflare Workers?
Cloudflare Workers 是一個創新的無服務器計算平台,讓開發人員可以將代碼直接部署到 Cloudflare 的廣泛網絡,該網絡遍布全球 200 多個城市。 從本質上講,它使應用程序能夠盡可能靠近最終用戶運行,從而減少延遲並增強用戶體驗。
以下是其功能和優點的概述:
- 無服務器執行環境:Cloudflare Workers 在無服務器環境中運行,這意味著開發人員無需管理或維護任何服務器。 相反,他們可以專注於編寫代碼,而平台則負責從分發到擴展的其餘部分。
- 邊緣計算:與應用程序在單個服務器或數據中心上運行的傳統模型不同,Cloudflare Workers 將您的代碼帶到 Cloudflare 網絡的邊緣。 這可確保您的應用程序運行時更貼近用戶,從而提高性能和速度。
- 語言靈活性:Workers 使用 V8 JavaScript 引擎,與 Chrome 使用的運行時相同,允許開發人員使用 JavaScript 編寫代碼。 此外,由於 WebAssembly 支持,還可以使用 Rust、C 和 C++ 等其他語言。
- 安全性:通過利用 Cloudflare 網絡固有的安全性,Workers 有助於保護應用程序免受 DDoS 攻擊等各種威脅。
Cloudflare Workers 為希望增強應用程序性能、可靠性和安全性的開發人員提供了創新且高度可擴展的解決方案。
在 Cloudflare Pages 中,Workers 位於名為functions
的目錄中。 我已將所有 JavaScript/TypeScript 代碼放置在這個空間中,利用 Workers 提供的全面功能。
什麼是 Cloudflare Workers KV?
Cloudflare Workers KV (Key-Value) 是一個全球分佈式、最終一致的鍵值存儲系統,允許您從Cloudflare Workers 腳本中的任何位置存儲和訪問數據。 它旨在幫助您擴展和簡化無服務器環境中的狀態管理。
以下是其主要特性和優點:
- 全球分佈:Cloudflare Workers KV 構建在 Cloudflare 網絡之上,該網絡覆蓋全球 300 多個城市。 這可確保您的數據在用戶附近存儲和訪問,從而減少延遲並提高應用程序的整體性能。
- 快速讀寫:Workers KV 提供適合各種應用的低延遲數據訪問。 雖然寫入需要更長的時間才能在全局範圍內傳播(通常在幾秒鐘內),但讀取操作通常速度很快,因此非常適合讀取密集型工作負載。
- 大規模:您可以在單個 Workers KV 命名空間中存儲數十億個密鑰,每個密鑰可以容納高達 25MB 的值。
- 命名空間:KV 命名空間是鍵值對的容器。 它們允許您在 Workers KV 存儲中隔離不同的數據集,這在管理多個應用程序或環境(例如登台和生產)時特別有用。
- 最終一致性:Workers KV 使用最終一致性。 這意味著數據更新(寫入)將在全球範圍內傳播並隨著時間的推移變得一致,這通常只需幾秒鐘。
Cloudflare Workers KV 提供了一種獨特的解決方案,用於在無服務器環境中管理狀態,為開發人員提供可靠、快速且全球分佈式的數據存儲系統。
在開發這個用戶註冊系統時,我戰略性地設計了以下 Workers KV 命名空間:
- USERS:這是所有用戶的主存儲。 它旨在處理本質上無限數量的記錄。
- USERS_LOGIN_HISTORY:記錄登錄活動的專用空間,使用戶能夠定期評估其帳戶的安全足跡。
- USERS_SESSIONS:此命名空間捕獲有關當前登錄用戶的詳細信息,包括唯一 ID、設備、位置等。
- USERS_SESSIONS_MAPPING:由於 Workers KV 的最終一致性模型,寫入“USERS_SESSIONS”和檢查它之間可能存在延遲。 如果操作發生在不同的邊緣位置,則這種情況尤其可能發生。 為了避免這種情況,在驗證後,我直接將新的會話 UID 添加到 USERS_SESSIONS_MAPPING,確保在寫入“USERS_SESSIONS”之前就將其包含在內。
- USERS_TEMP:我使用此命名空間作為臨時(臨時)鏈接和其他具有預定到期時間的內容的存儲庫。
我們創建了具有無限容量、自動擴展和高可用性的數據庫——這些功能通常存在於昂貴的數據庫中。
設計項目
我的目標是在不依賴第三方庫的情況下製作一些簡單有效的東西,並且我成功了。 整個項目結構是這樣展開的:
1+---framework
2| +---models
3| | password.ts
4| | user.ts
5| +---templates
6| | \---emails
7| | password-reset.ts
8| | password-updated.ts
9| \---utils
10| encryptor.ts
11| index.ts
12| mailer.ts
13| validators.ts
14+---functions
15| | forgot-password.ts
16| | login.ts
17| | logout.ts
18| | password-reset.ts
19| | register.ts
20| | _middleware.ts
21| \---user
22| dashboard.ts
23| _middleware.ts
24\---public
25 | forgot-password.html
26 | index.html
27 | login.html
28 | logout.html
29 | password-reset.html
30 | register.html
31 \---user
32 dashboard.html
為了讓您清楚地了解上面的架構,讓我們深入研究一下詳細的細分:
- framework:該目錄包含我們的基礎 TypeScript 代碼。 從數據模型到電子郵件模板的所有內容都位於此處,確保整個系統採用一致的方法。
- functions:在這裡,您將找到專為 Cloudflare Pages 函數定制的 TypeScript 代碼,從而簡化了站點的後端操作。
- public:我們所有可公開訪問的靜態 HTML 文件都駐留在此文件夾中,形成用戶可見的界面。
簡而言之,當您導航到login.html
頁面時,Cloudflare Pages 就會啟動,執行相應的login.ts
代碼。 這種動態相互作用在所有頁面及其相關功能中持續存在。
通過這種設置,我們可以無縫地處理一系列任務。 無論是內容重寫、數據處理還是通過 Cloudflare Workers KV 獲取數據,一切都得到有效管理。
搭建用戶註冊系統
在開始我們的旅程之前,我們將首先建立一個用戶註冊系統。 這是核心功能。
- 我們的第一步是設計一個簡單的 HTML 表單並將其放置在
register.html
中,然後編寫負責處理所提供數據的函數:
1<form class="row g-3 needs-validation" id="register" method="POST" novalidate>
2 <div class="col-md-6 mb-3">
3 <label for="firstName" class="form-label">First Name</label>
4 <input type="text" class="form-control" name="firstName" id="firstName" autocomplete="given-name" placeholder="Required" required>
5 <div class="invalid-feedback">You must enter your First Name.</div>
6 </div>
7 <div class="col-md-6 mb-3">
8 <label for="lastName" class="form-label">Last Name</label>
9 <input type="text" class="form-control" name="lastName" id="lastName" autocomplete="family-name" placeholder="Required" required>
10 <div class="invalid-feedback">You must enter your Last Name.</div>
11 </div>
12 <div class="col-md-12 mb-3">
13 <label for="email" class="form-label">Email</label>
14 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
15 <div class="invalid-feedback">You must enter your Email.</div>
16 </div>
17 <div class="col-md-12 mb-3">
18 <label for="company" class="form-label">Company</label>
19 <input type="text" class="form-control" name="company" id="company" autocomplete="organization" placeholder="Optional">
20 <div class="invalid-feedback">You must enter your Company.</div>
21 </div>
22 <div class="col-md-6 mb-3">
23 <label for="password" class="form-label">Password</label>
24 <input type="password" class="form-control" name="password" id="password" autocomplete="new-password" placeholder="Required" required>
25 <div class="invalid-feedback">You must enter a Password.</div>
26 </div>
27 <div class="col-md-6 mb-3">
28 <label for="confirm_password" class="form-label">Confirm Password</label>
29 <input type="password" class="form-control" name="confirm_password" id="confirm_password" autocomplete="new-password" placeholder="Required" required>
30 <div class="invalid-feedback">Your Password does not match.</div>
31 </div>
32 <div class="col-md-12 mb-3">
33 <div class="form-check">
34 <input class="form-check-input" type="checkbox" name="terms" id="terms" required>
35 <label class="form-check-label" for="terms">
36 I have read and agree to the <a href="#" class="link-primary">Terms of Service</a>, <a href="#" class="link-primary">Privacy Policy</a> and <a href="#" class="link-primary">Cookies Policy</a>.
37 </label>
38 <div class="invalid-feedback">You must agree before registering.</div>
39 </div>
40 </div>
41 <div class="col-md-6 mx-auto mb-3">
42 <div class="d-grid gap-2">
43 <button class="btn btn-primary" type="submit">Create Account</button>
44 </div>
45 </div>
46</form>
設置好表單後,下一步是使用 JavaScript 增強數據提交。 雖然傳統的表單提交完全可行,但我在示例中合併了 Bootstrap 5 驗證,因此傾向於 AJAX 來發布數據。
這是我的演示中的一個工作示例:
1(function () {
2 'use strict'
3
4 // Check if passwords match
5 document.querySelectorAll("#password, #confirm_password").forEach(function (input) {
6 input.addEventListener("keyup", function () {
7 const password = document.getElementById("password");
8 const confirmPassword = document.getElementById("confirm_password");
9 if (
10 password.value !== "" &&
11 confirmPassword.value !== "" &&
12 confirmPassword.value === password.value
13 ) {
14 // Do something when passwords match
15 }
16 });
17 });
18
19 var form = document.querySelector("#register");
20
21 form.addEventListener('submit', function (event)
22 {
23 const _alert = document.querySelector("div.alert.alert-danger");
24
25 if(_alert)
26 _alert.remove();
27
28 if (!form.checkValidity())
29 {
30 // Prevent default form submission
31 event.preventDefault();
32 event.stopPropagation();
33 form.classList.add('was-validated');
34 }
35 else
36 {
37 // Mark inputs as validated
38 form.classList.add('was-validated');
39
40 // Prevent default form submission again
41 event.preventDefault();
42 event.stopPropagation();
43
44 // Helper to get all inputs we want
45 const getAllFormElements = element => Array.from(element.elements).filter(tag => ["input"].includes(tag.tagName.toLowerCase()));
46
47 // Grab desired inputs
48 const registerFormElements = getAllFormElements(event.currentTarget);
49
50 // Loop over them and disable
51 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
52 element.setAttribute("readonly", true);
53 element.style = 'background-color: #e9ecef; opacity: 1;';
54 });
55
56 // Disable button and show loading spinner
57 let _button = event.currentTarget.querySelector(".btn");
58 _button.setAttribute("disabled", true);
59 _button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Please wait...';
60
61 // A modern replacement for XMLHttpRequest.
62 // https://caniuse.com/fetch
63 if (window.fetch)
64 {
65 (async() => {
66 await fetch(window.location.href, {
67 method: 'POST',
68 // NOTE: FormData will only grab inputs with name attribute
69 body: JSON.stringify(Object.fromEntries(new FormData(event.target))),
70 headers: {
71 'Accept': 'application/json',
72 'Content-Type': 'application/json'
73 }
74 }).then((response) => {
75 // Pass both the parsed JSON and the status code to the next .then()
76 return response.json().then((data) => ({status: response.status, body: data}));
77 }).then(({status, body}) => {
78
79 if (body.success === true && status === 201) {
80 Array.prototype.slice.call(document.querySelectorAll('form *')).forEach(function (element) {
81 element.style.display = "none";
82 });
83
84 let _alert = document.createElement("div");
85 _alert.classList.add('alert');
86 _alert.classList.add('alert-success');
87 _alert.setAttribute("role", 'alert');
88 _alert.innerHTML = '<p>Thank you for joining! A confirmation email has been sent out. You need to verify your email before trying to log in.</p>';
89
90 form.prepend(_alert);
91 } else {
92 let _alert = document.createElement("div");
93 _alert.classList.add('alert');
94 _alert.classList.add('alert-danger');
95 _alert.setAttribute("role", 'alert');
96 _alert.innerText = `Error #${data.error.code}: ${data.error.message}`;
97
98 form.prepend(_alert);
99
100 form.classList.remove('was-validated');
101
102 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
103 element.removeAttribute("style");
104 element.removeAttribute("readonly");
105 });
106
107 _button.removeAttribute("disabled");
108 _button.innerHTML = 'Create Account';
109 }
110 });
111 })();
112 }
113 else
114 {
115 alert("Your browser is too old!\nIt does not have the most recent features.\nPlease update your browser or use a different one.");
116 }
117 }
118 }, false);
119})();
單擊“創建帳戶”後,數據將通過 AJAX 發佈到/register
,從而觸發functions/register.ts
代碼。 該機制允許 GET 和 POST 數據處理。 雖然管理 OPTIONS 是可能的,但我們將在本次討論中將其放在一邊。
- 對於標準頁面訪問或 GET 執行,有可能引入大量高級功能,例如 CSRF 保護。 讓我們看一下下面的例子:
1/**
2 * GET /register
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // Fetch the original page content
7 const response = await next();
8
9 // Prepare our CSRF data
10 const IP = request.headers.get('CF-Connecting-IP');
11 const Country = request.headers.get('CF-IPCountry') || '';
12 const UserAgent = request.headers.get('User-Agent');
13 const expiry = Date.now() + 60000;
14 const CSRF_TOKEN = JSON.stringify({i: IP, c: Country, u: UserAgent, e: expiry});
15 const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
16 const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
17
18 // Rewrite the content and stream it back to the user (async)
19 return new HTMLRewriter()
20 .on("form", {
21 element(form) {
22 // The CSRF input
23 form.append(
24 `<input type="hidden" name="csrf" value="${hex}" />`,
25 { html: true }
26 );
27 },
28 })
29 .transform(response);
30};
在此過程中,我們使用Cloudflare Workers HTMLRewriter 功能為每個頁面請求生成唯一的CSRF 令牌並將其嵌入到表單中。
我將在後續部分中更深入地研究我的 CSRF 策略。 暫時觀察我如何動態附加唯一的隨機 CSRF 代碼來增強安全性。
- 現在,讓我們轉向 POST 執行階段。 在這裡,我們仔細驗證輸入數據,安全存儲,然後向用戶發送電子郵件以進行確認或進一步說明。 為了向您提供概念性概述,我起草了一個偽代碼表示:
1/**
2 * POST /register
3 */
4export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) =>
5{
6 // 驗證內容類型
7 // 在做任何事情之前驗證我們的 CSRF
8 // 檢查現有用戶電子郵件
9 // 生成新的鹽並對原始密碼進行哈希處理
10 // 存儲用戶一些元數據
11 // ETC
12}
在我的配置中,成功註冊後,用戶的詳細信息將存儲在 Workers KV 中,如下所示:
1{
2 "uid": "888e8801-3b35-4dd3-9ae2-7ff0f564bb3b",
3 "email": "[email protected]",
4 "firstname": "John",
5 "lastname": "Smith",
6 "company": "",
7 "role": "User",
8 "password": "....",
9 "salt": "...",
10 "access_token": null,
11 "created_at": 123456789,
12 "confirmed_at": null,
13 "last_updated": null,
14 "last_sign_in_at": 123456789,
15 "recovery_sent_at": null,
16 "password_changed_at": null
17}
您可以調整此格式,根據需要添加或刪除字段。 我必須強調,我不會提供完整的、可直接複製的代碼。 投資於理解和實踐探索至關重要。 只有這樣,一個人才能真正成長為一名開發人員。
構建用戶登錄系統
過渡到 Cloudflare Pages 企業的登錄機制非常簡單。 我們將採用與註冊過程類似的方法。
- 正如我們製作註冊表單一樣,為登錄過程製作一個簡潔的表單並將其放置在“login.html”中:
1<form class="row g-3 needs-validation" id="login" method="POST" novalidate>
2 <div class="col-sm-12">
3 <label for="email" class="form-label">Email</label>
4 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
5 <div class="invalid-feedback">You must enter your Email.</div>
6 </div>
7 <div class="col-sm-12">
8 <label for="password" class="form-label">Password</label>
9 <input type="password" class="form-control" name="password" id="password" autocomplete="password" placeholder="Required" required>
10 <div class="invalid-feedback">You must enter a Password.</div>
11 </div>
12 <div class="d-grid gap-2 col-sm-6 mx-auto">
13 <button class="btn btn-primary" type="submit">Login</button>
14 </div>
15</form>
- 設置表單後,就可以處理其渲染了。 後續步驟涉及創建必要的代碼。 這將從
login.ts
自動觸發:
1/**
2 * GET /login
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // 獲取原始頁面內容
7 // 準備我們的CSRF數據
8 // 重寫內容並將其流式傳輸回用戶(異步)
9};
- 最後一步是製定代碼來管理 POST 執行,位於
login.ts
中:
1/**
2 * POST /login
3 */
4export const onRequestPost: PagesFunction<{
5 USERS: KVNamespace;
6 USERS_SESSIONS: KVNamespace;
7 USERS_SESSIONS_MAPPING: KVNamespace;
8 USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) =>
9{
10 // 驗證內容類型
11 // 在做任何事情之前驗證我們的 CSRF
12 // 檢查現有用戶電子郵件
13 // 生成新的鹽並對原始密碼進行哈希處理
14 // 比較密碼
15 // 保存會議
16 // 更新歷史記錄
17 // 檢索該用戶的當前映射(如果存在)
18 // 將新的會話 UID 添加到映射中
19 // 存儲更新後的映射
20 // ETC
21};
通過這 3 個簡化的步驟,我們無縫地構建了登錄系統。 不可否認,登錄機制是最複雜的部分,需要執行註釋中的多項操作。 然而,不要將此視為一個複雜的問題,而是一個需要克服的令人振奮的挑戰!
構建用戶密碼重置系統
- 與我們之前的組件相似,我們首先設計用於啟動密碼重置的專用表單:
1<form class="row g-3 needs-validation" id="forgot" novalidate>
2 <div class="col-sm-12">
3 <label for="email" class="form-label">Email</label>
4 <input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required />
5 <div class="invalid-feedback">You must enter your Email.</div>
6 </div>
7
8 <div class="d-grid gap-2 col-sm-6 mx-auto">
9 <button class="btn btn-primary" type="submit">Request</button>
10 </div>
11</form>
基本概念很簡單:輸入用戶的電子郵件後,如果我們的系統能夠識別該電子郵件,則會生成並發送一個單一的密碼重置鏈接。 為了增強安全性,請確保此鏈接在短時間內保持活動狀態,最好不超過 2 小時。
- 開始編寫專門用於管理 GET 執行的代碼,您需要將其嵌入到
forgot-password.ts
中:
1/**
2 * GET /forgot-password
3 */
4export const onRequestGet: PagesFunction = async ({ next }) =>
5{
6 // 獲取原始頁面內容
7 // 準備我們的CSRF數據
8 // 重寫內容並將其流式傳輸回用戶(異步)
9};
- 繼續制定負責 POST 執行的代碼,確保其位於同一個
forgot-password.ts
文件中:
1/**
2 * POST /forgot-password
3 */
4export const onRequestPost: PagesFunction<{
5 USERS: KVNamespace;
6 USERS_TEMP: KVNamespace;
7 }> = async ({ request, env }) =>
8{
9 // 驗證內容類型
10 // 在做任何事情之前驗證我們的 CSRF
11 // 檢查現有用戶電子郵件
12 // 生成一個唯一的字符串進行哈希
13 // 散列字符串
14 // 保存到KV,過期2小時
15 // 發送郵件給用戶
16 // ETC
17};
現在由您來完成編碼難題。 通過遵循我的評論中的指導,您應該會發現該過程非常易於管理。 完成後,您將配備一個完全可操作的密碼重置系統。
注意事項:不要顯示諸如“不存在此類電子郵件”之類的消息。 這些指標對於黑客來說是金礦,為潛在的暴力攻擊鋪平了道路。 相反,採用一種更加模糊但用戶友好的方法:“如果電子郵件在我們的系統中註冊,您將收到一個重置鏈接。”
用戶註冊演示
探索我建立的用戶註冊流程的現場演示:https://members.mecanik.dev/
特點包括:
我目前正在處理其他項目,但我將繼續增強這個平台。 請隨意測試 - 只要確保您使用真實的電子郵件即可,以避免影響我的 SendGrid 聲譽。
我歡迎任何反饋或報告的問題。 有的話請聯繫!
問題與解答
毫無疑問,安全是首要考慮因素。 您可能會質疑系統針對潛在攻擊和數據洩露的穩健性。
考慮到 Cloudflare Pages 在 Cloudflare 平台上運行,允許您訪問其高級 Web 應用程序防火牆 (WAF) 和一套安全工具。
CSRF 實際上是如何運作的?
您可能已經觀察到每次加載時都會動態生成並將 CSRF 令牌 注入到表單中。 以下是該機制如何確保請求的安全性:
CSRF 令牌組件:
- IP:捕獲用戶當前的 IP 地址。
- Country:標識用戶當前所在位置。
- UserAgent:記錄用戶的瀏覽器詳細信息。
- expiry:通過在當前時間上添加一分鐘來設置計時器。
該數據以 JSON 格式組裝:{i, c, u, e}
,隨後被加密並轉換為十六進制:
1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
關於加密功能:
- 使用用戶定義的密碼對數據進行加密,生成密碼密鑰,然後從中導出 AES 密鑰。
- 該 AES 密鑰使用即時生成的鹽和 IV(初始化向量)對數據進行加密。
- 加密輸出是一個ArrayBuffer,封裝了迭代次數、salt、IV 和加密數據。
簡而言之,此加密過程遵循行業標準實踐,使加密數據既難以破譯又防止篡改。
驗證 CSRF 令牌:
1const unhex = Utils.HexToBytes(formData.csrf);
2const decrypted = new TextDecoder().decode(await decryptData(unhex, env.CSRF_SECRET));
3const parsed = JSON.parse(decrypted);
4
5if (!Utils.isCRSFData(parsed))
6{
7 return new Response(JSON.stringify({
8 result: null,
9 success: false,
10 // An ambigous message; don't tell the hacker what's missing.
11 error: { code: 1001, message: "CSRF 令牌無效。 請刷新頁面並重試。" }
12 }),
13 {
14 status: 403,
15 headers: { 'content-type': 'application/json;charset=UTF-8'
16 }
17 });
18}
19
20const IP = request.headers.get('CF-Connecting-IP');
21const Country = request.headers.get('CF-IPCountry') || '';
22const UserAgent = request.headers.get('User-Agent');
23
24if(IP !== parsed.i || Country !== parsed.c || UserAgent !== parsed.u || Date.now() > parsed.e)
25{
26 return new Response(JSON.stringify({
27 result: null,
28 success: false,
29 // An ambigous message; don't tell the hacker what's missing.
30 error: { code: 1002, message: "CSRF 令牌無效。 請刷新頁面並重試。" }
31 }),
32 {
33 status: 403,
34 headers: { 'content-type': 'application/json;charset=UTF-8'
35 }
36 });
37}
驗證過程嚴格檢查 CSRF 令牌,確保其:
- 源自同一 IP 地址。
- 從同一國家發貨。
- 從同一瀏覽器發送。
- 在有效時間內提交。
- 如果這些檢查中的任何一個失敗,系統會將 CSRF 令牌識別為無效,從而提供針對潛在威脅的強大保護。
如果這些檢查中的任何一個失敗,系統都會將 CSRF 令牌識別為無效,從而提供針對潛在威脅的強大保護。
密碼散列/加密
對於密碼散列/加密,我採用了 PBKDF2 (基於密碼的密鑰派生函數 2)與 [SHA-256](https://en.wikipedia.org/wiki/SHA-2 )哈希算法。
該方法為每個密碼使用獨特的、加密安全的偽隨機“鹽”,以確保增強的安全性。 這種方法可以防止彩虹表和暴力攻擊。
存儲密碼的散列版本,而不是明文,進一步增強數據完整性。
為什麼不使用 JWT 令牌?
JWT(JSON Web 令牌)是一種流行的方法,用於以緊湊、URL 安全的方式處理用戶身份驗證並在各方之間傳輸信息。
雖然它們有很多優點,但人們可能有理由選擇不在特定環境中使用它們,例如完整的用戶註冊系統。 下面是更深入的了解:
- 無狀態和撤銷:JWT 的標誌之一是其無狀態。 然而,這種性質可能會給用戶管理系統帶來問題。 例如,如果用戶的 JWT 令牌被盜或洩露,則沒有直接的方法可以撤銷該令牌,除非您維護令牌黑名單,這違背了無狀態的目的。
- 大小:當您向 JWT 添加更多數據時,它的大小會增加。 如果您有一個全面的用戶註冊系統,您可能需要在令牌中存儲更多與用戶相關的數據,這可能會導致更大的 HTTP 標頭和增加的延遲。
- 存儲安全:對於JWT的客戶端存儲,常見的存儲位置包括本地存儲或cookie。 本地存儲很容易受到 XSS 攻擊,雖然 cookie 可以在更大程度上得到保護,但如果處理不當,它們會為 CSRF 攻擊開闢途徑。
- 過期處理:管理 JWT 的過期可能很複雜。 雖然短期令牌可以降低風險,但它們需要刷新機制,這會給身份驗證流程帶來更多複雜性。
- 沒有內置撤銷:如前所述,如果令牌被洩露,則沒有固有的機制可以撤銷或使其失效,直到其過期。 如果令牌的使用壽命很長,這可能會帶來重大的安全風險。
- 開發人員的複雜性:對於那些不熟悉 JWT 的人來說,了解如何正確生成、驗證和使用它們需要一個學習曲線。 如果不徹底理解和實施,這種複雜性可能會帶來錯誤和漏洞。
在我對各種系統及其對用戶數據的處理的探索中,我發現一些公司在其 JWT 令牌中存儲了大量信息,包括用戶角色等敏感細節。
需要明確的是,我的目的並不是批評或誹謗其他方法。 然而,根據我對安全的專業立場,將此類關鍵信息直接嵌入代幣中會引起擔憂。 暴露過多的數據存在固有風險,特別是當這些數據可能被利用來推斷用戶權限或其他功能時。
考慮到這些保留,我有意識地決定放棄這種方法。 相反,我傾向於將經過驗證的服務器端身份驗證與安全 cookie 處理相結合。 在我看來,這種方法在用戶體驗和強大的安全性之間取得了平衡,確保敏感數據受到保護,並且系統對潛在漏洞保持彈性。
可靠地發送電子郵件
我選擇了帶有動態模板的 SendGrid,因為它被廣泛採用並且我對其細微差別非常熟悉。 要更深入地了解,請參閱他們的 API 文檔 。
雖然它的集成可能會帶來一些複雜性,但這都是有益挑戰的一部分。 這是一個供您參考的示例:
1const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send';
2const SENDGRID_API_KEY = 'YOUR_SENDGRID_API_KEY'; // 替換為您的實際 API 密鑰
3
4interface EmailDetails {
5 to: string;
6 from: string;
7 subject: string;
8 content: string;
9}
10
11async function sendEmail(details: EmailDetails): Promise<void> {
12 const data = {
13 personalizations: [{
14 to: [{
15 email: details.to
16 }]
17 }],
18 from: {
19 email: details.from
20 },
21 subject: details.subject,
22 content: [{
23 type: 'text/plain',
24 value: details.content
25 }]
26 };
27
28 try {
29 const response = await fetch(SENDGRID_API_URL, {
30 method: 'POST',
31 headers: {
32 'Authorization': `Bearer ${SENDGRID_API_KEY}`,
33 'Content-Type': 'application/json'
34 },
35 body: JSON.stringify(data)
36 });
37
38 if (response.status === 202) {
39 console.log('郵件發送成功!');
40 } else {
41 const responseBody = await response.json();
42 console.error('發送郵件失敗:', responseBody.errors);
43 }
44 } catch (error) {
45 console.error('發送電子郵件時出錯:', error);
46 }
47}
48
49// 使用示例:
50sendEmail({
51 to: '[email protected]',
52 from: '[email protected]',
53 subject: 'Hello from SendGrid!',
54 content: '這是使用 SendGrid API 發送的測試電子郵件。'
55});
如何進一步提高安全性?
儘管 Cloudflare Workers KV 會對存儲的數據進行加密,但您可以為用戶數據添加額外的加密層。
或者想為潛在的攻擊者添加一些變化? 考慮實施虛擬表單輸入來迷惑機器人和黑客:
1return new HTMLRewriter()
2 .on("form",
3 {
4 element(form)
5 {
6 // 虛擬輸入...只是為了讓黑客更加頭疼並使他困惑:)
7 const randomNumber = Utils.RandomNumber(1, 5);
8
9 for (let i = 0; i < randomNumber; i++)
10 {
11 form.append(
12 `<input type="hidden" name="${Utils.RandomString(Utils.RandomNumber(i + 5, i + 25))}" value="${Utils.RandomString(Utils.RandomNumber(i + 25, i + 256))}" />`,
13 { html: true }
14 );
15 }
16 },
17 })
18.transform(response);
此代碼片段在每次頁面加載時動態生成 1 到 5 個隨機命名的輸入。
功能幾乎是無限的,你只需要發揮你的想像力。
包起來
我們終於得到它了! 我真誠地希望這篇文章對您來說既富有洞察力又有吸引力。 無論您是經驗豐富的開發人員還是剛剛進入可擴展云網站的世界,共享知識對於我們社區的發展都至關重要。
如果您發現本文有價值,請考慮將其轉發給其他開發人員或對該主題感興趣的人。 每一次分享都會拓寬我們的集體理解。
非常感謝您的反饋和問題。 請隨時發表評論或聯繫我們——讓我們繼續對話。 安全編碼!
評論