在不斷發展的 Web 開發領域,無服務器應用程序正在穩步留下自己的印記。 它們無可否認的優勢,例如無與倫比的可擴展性和強大的性能,使它們脫穎而出。 無服務器的美妙之處在於它的承諾:提供強大的解決方案,不僅可以輕鬆擴展,而且預算友好。

您是否曾經夢想過推出一個商業創意,但卻被創建用戶註冊和登錄系統的技術細節所阻礙? 雖然 Netlify 等平台和各種基於雲的數據庫確實存在,但它們無法與 Cloudflare 提供的功能相比。 許多替代方案在擴展時可能會增加成本,但對於 Cloudflare Pages,情況有所不同。

在過去的幾個月裡,我一直斷斷續續地致力於這個項目和演示,同時兼顧我的其他承諾。 我對等待表示歉意,尤其是對那些熱切期待 這個系統的人。

釋放 Cloudflare 頁面的力量

  • 無縫可擴展性:輕鬆管理無限的用戶註冊。
  • 成本效益:告別意外的管理費用; 享受一致的定價。
  • 極速:體驗前所未有的性能。
  • 可靠的安全性:您的用戶數據仍然受到保護且安全。

我們將使用什麼

  • Cloudflare 頁面
  • Cloudflare 頁面功能
  • Cloudflare Workers KV

什麼是 Cloudflare 頁面?

Cloudflare Pages 是一個現代、用戶友好的平台,供開發人員構建、部署和託管其網站。 它提供與 GitHub 的無縫集成,這意味著您只需將代碼推送到 GitHub,Cloudflare Pages 將處理其餘的工作 - 構建、部署,甚至更新。

它的工作原理如下:

  1. 集成工作流程:Cloudflare Pages 圍繞 git 工作流程構建。 將 GitHub 存儲庫連接到 Cloudflare Pages 後,每次您推送到所選分支時,它都會開始構建和部署您的站點。
  2. JAMstack 優化:Cloudflare Pages 支持 JAMstack 原則,這意味著您可以使用您喜歡的靜態站點生成器或 JavaScript 框架(包括但不限於 Jekyll、Hugo、Next.js 和 React)構建站點。
  3. 快速、安全的交付:Pages 由全球分佈的 Cloudflare 網絡提供支持,確保您的網站可用且快速,無論您的受眾身在何處。 此外,Cloudflare 固有的安全功能可保護您的網站免受威脅。
  4. 持續部署:每次您在 GitHub 存儲庫上進行更新時,Cloudflare Pages 都會自動構建和部署您的網站。 這使您可以快速迭代並使部署過程變得輕而易舉。
  5. 自定義域和 HTTPS:使用 Pages,您可以將自定義域連接到您的站點,並且它在所有站點上提供免費、自動的 HTTPS,以確保連接始終安全。
  6. 預覽部署:每當您在鏈接的 GitHub 存儲庫中創建新的拉取請求時,Cloudflare Pages 都會自動生成一個唯一的預覽 URL,以便您在上線之前查看更改。

無論您是獨立開發者還是大型團隊的一員,Cloudflare Pages 都提供了一種簡單、快速且安全的方式讓您的網站上線。

鑑於上述情況,對於這個用戶註冊系統,我選擇了純粹且直接的 HTML 頁面,避開任何額外的框架或構建工具。 這種方法確保了無與倫比的簡單性,並提供了實現任何期望結果的靈活性。

什麼是 Cloudflare Workers?

Cloudflare Workers 是一個創新的無服務器計算平台,讓開發人員可以將代碼直接部署到 Cloudflare 的廣泛網絡,該網絡遍布全球 200 多個城市。 從本質上講,它使應用程序能夠盡可能靠近最終用戶運行,從而減少延遲並增強用戶體驗。

以下是其功能和優點的概述:

  1. 無服務器執行環境:Cloudflare Workers 在無服務器環境中運行,這意味著開發人員無需管理或維護任何服務器。 相反,他們可以專注於編寫代碼,而平台則負責從分發到擴展的其餘部分。
  2. 邊緣計算:與應用程序在單個服務器或數據中心上運行的傳統模型不同,Cloudflare Workers 將您的代碼帶到 Cloudflare 網絡的邊緣。 這可確保您的應用程序運行時更貼近用戶,從而提高性能和速度。
  3. 語言靈活性:Workers 使用 V8 JavaScript 引擎,與 Chrome 使用的運行時相同,允許開發人員使用 JavaScript 編寫代碼。 此外,由於 WebAssembly 支持,還可以使用 Rust、C 和 C++ 等其他語言。
  4. 安全性:通過利用 Cloudflare 網絡固有的安全性,Workers 有助於保護應用程序免受 DDoS 攻擊等各種威脅。

Cloudflare Workers 為希望增強應用程序性能、可靠性和安全性的開發人員提供了創新且高度可擴展的解決方案。

在 Cloudflare Pages 中,Workers 位於名為functions的目錄中。 我已將所有 JavaScript/TypeScript 代碼放置在這個空間中,利用 Workers 提供的全面功能。

什麼是 Cloudflare Workers KV?

Cloudflare Workers KV (Key-Value) 是一個全球分佈式、最終一致的鍵值存儲系統,允許您從Cloudflare Workers 腳本中的任何位置存儲和訪問數據。 它旨在幫助您擴展和簡化無服務器環境中的狀態管理。

以下是其主要特性和優點:

  1. 全球分佈:Cloudflare Workers KV 構建在 Cloudflare 網絡之上,該網絡覆蓋全球 300 多個城市。 這可確保您的數據在用戶附近存儲和訪問,從而減少延遲並提高應用程序的整體性能。
  2. 快速讀寫:Workers KV 提供適合各種應用的低延遲數據訪問。 雖然寫入需要更長的時間才能在全局範圍內傳播(通常在幾秒鐘內),但讀取操作通常速度很快,因此非常適合讀取密集型工作負載。
  3. 大規模:您可以在單個 Workers KV 命名空間中存儲數十億個密鑰,每個密鑰可以容納高達 25MB 的值。
  4. 命名空間:KV 命名空間是鍵值對的容器。 它們允許您在 Workers KV 存儲中隔離不同的數據集,這在管理多個應用程序或環境(例如登台和生產)時特別有用。
  5. 最終一致性: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 獲取數據,一切都得到有效管理。

搭建用戶註冊系統

在開始我們的旅程之前,我們將首先建立一個用戶註冊系統。 這是核心功能。

  1. 我們的第一步是設計一個簡單的 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 是可能的,但我們將在本次討論中將其放在一邊。

  1. 對於標準頁面訪問或 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 代碼來增強安全性。

  1. 現在,讓我們轉向 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 企業的登錄機制非常簡單。 我們將採用與註冊過程類似的方法。

  1. 正如我們製作註冊表單一樣,為登錄過程製作一個簡潔的表單並將其放置在“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>
  1. 設置表單後,就可以處理其渲染了。 後續步驟涉及創建必要的代碼。 這將從login.ts自動觸發:
1/**
2 * GET /login
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// 獲取原始頁面內容
7	// 準備我們的CSRF數據
8	// 重寫內容並將其流式傳輸回用戶(異步)
9};
  1. 最後一步是製定代碼來管理 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. 與我們之前的組件相似,我們首先設計用於啟動密碼重置的專用表單:
 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 小時。

  1. 開始編寫專門用於管理 GET 執行的代碼,您需要將其嵌入到 forgot-password.ts 中:
1/**
2 * GET /forgot-password
3 */
4export const onRequestGet: PagesFunction = async ({ next }) => 
5{
6	// 獲取原始頁面內容
7	// 準備我們的CSRF數據
8	// 重寫內容並將其流式傳輸回用戶(異步)
9};
  1. 繼續制定負責 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));

關於加密功能

  1. 使用用戶定義的密碼對數據進行加密,生成密碼密鑰,然後從中導出 AES 密鑰。
  2. 該 AES 密鑰使用即時生成的鹽和 IV(初始化向量)對數據進行加密。
  3. 加密輸出是一個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 安全的方式處理用戶身份驗證並在各方之間傳輸信息。

雖然它們有很多優點,但人們可能有理由選擇不在特定環境中使用它們,例如完整的用戶註冊系統。 下面是更深入的了解:

  1. 無狀態和撤銷:JWT 的標誌之一是其無狀態。 然而,這種性質可能會給用戶管理系統帶來問題。 例如,如果用戶的 JWT 令牌被盜或洩露,則沒有直接的方法可以撤銷該令牌,除非您維護令牌黑名單,這違背了無狀態的目的。
  2. 大小:當您向 JWT 添加更多數據時,它的大小會增加。 如果您有一個全面的用戶註冊系統,您可能需要在令牌中存儲更多與用戶相關的數據,這可能會導致更大的 HTTP 標頭和增加的延遲。
  3. 存儲安全:對於JWT的客戶端存儲,常見的存儲位置包括本地存儲或cookie。 本地存儲很容易受到 XSS 攻擊,雖然 cookie 可以在更大程度上得到保護,但如果處理不當,它們會為 CSRF 攻擊開闢途徑。
  4. 過期處理:管理 JWT 的過期可能很複雜。 雖然短期令牌可以降低風險,但它們需要刷新機制,這會給身份驗證流程帶來更多複雜性。
  5. 沒有內置撤銷:如前所述,如果令牌被洩露,則沒有固有的機制可以撤銷或使其失效,直到其過期。 如果令牌的使用壽命很長,這可能會帶來重大的安全風險。
  6. 開發人員的複雜性:對於那些不熟悉 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 個隨機命名的輸入。

功能幾乎是無限的,你只需要發揮你的想像力。

包起來

我們終於得到它了! 我真誠地希望這篇文章對您來說既富有洞察力又有吸引力。 無論您是經驗豐富的開發人員還是剛剛進入可擴展云網站的世界,共享知識對於我們社區的發展都至關重要。

如果您發現本文有價值,請考慮將其轉發給其他開發人員或對該主題感興趣的人。 每一次分享都會拓寬我們的集體理解。

非常感謝您的反饋和問題。 請隨時發表評論或聯繫我們——讓我們繼續對話。 安全編碼!