Trong bối cảnh phát triển web không ngừng thay đổi, các ứng dụng serverless liên tục khẳng định vị thế của mình. Những ưu điểm không thể phủ nhận như khả năng mở rộng vô song và hiệu suất mạnh mẽ khiến chúng nổi bật. Vẻ đẹp của serverless nằm ở lời hứa của nó: cung cấp các giải pháp mạnh mẽ không chỉ mở rộng dễ dàng mà còn tiết kiệm chi phí.
Bạn đã bao giờ mơ ước khởi động một ý tưởng kinh doanh nhưng lại do dự trước những phức tạp kỹ thuật của việc xây dựng hệ thống đăng ký và đăng nhập người dùng? Mặc dù các nền tảng như Netlify và nhiều cơ sở dữ liệu đám mây khác nhau tồn tại, nhưng chúng không thể sánh được với những gì Cloudflare mang lại. Trong khi nhiều lựa chọn thay thế có thể thấy chi phí tăng vọt khi mở rộng quy mô, tình hình lại khác với Cloudflare Pages.
Tôi đã làm việc không liên tục trên dự án và bản demo này trong vài tháng, xen kẽ với các công việc khác. Tôi xin lỗi vì sự chờ đợi, đặc biệt là những người đã mong chờ hệ thống này .
Giải phóng sức mạnh của Cloudflare Pages
- Khả năng mở rộng liền mạch: Xử lý đăng ký người dùng không giới hạn mà không gặp trở ngại.
- Hiệu quả chi phí: Nói lời tạm biệt với chi phí bất ngờ; tận hưởng giá cả nhất quán.
- Hiệu suất cực nhanh: Trải nghiệm hiệu suất chưa từng có.
- Bảo mật vững chắc: Dữ liệu người dùng của bạn được bảo vệ an toàn.
Các công nghệ chúng ta sẽ sử dụng
- Cloudflare Pages
- Cloudflare Pages Functions
- Cloudflare Workers KV
Cloudflare Pages là gì?
Cloudflare Pages là một nền tảng hiện đại, thân thiện với người dùng, được thiết kế cho các nhà phát triển để xây dựng, triển khai và lưu trữ website. Nó cung cấp tích hợp liền mạch với GitHub, nghĩa là tất cả những gì bạn cần làm là đẩy code lên GitHub, và Cloudflare Pages sẽ xử lý phần còn lại – build, triển khai và thậm chí cả cập nhật.
Đây là cách nó hoạt động:
- Quy trình làm việc tích hợp: Cloudflare Pages được xây dựng xung quanh quy trình git. Khi bạn kết nối một repository GitHub với Cloudflare Pages, nó sẽ tự động build và triển khai website mỗi khi bạn push lên branch đã chọn.
- Tối ưu cho JAMstack: Cloudflare Pages hỗ trợ các nguyên tắc JAMstack, nghĩa là bạn có thể xây dựng website với trình tạo trang tĩnh hoặc framework JavaScript yêu thích, bao gồm Jekyll, Hugo, Next.js, React và nhiều hơn nữa.
- Phân phối nhanh và an toàn: Được vận hành bởi mạng lưới phân tán toàn cầu của Cloudflare, Pages đảm bảo website của bạn luôn khả dụng và truy cập nhanh, bất kể khán giả ở đâu. Ngoài ra, các tính năng bảo mật độc quyền của Cloudflare bảo vệ website khỏi các mối đe dọa.
- Triển khai liên tục: Cloudflare Pages tự động build và triển khai website mỗi khi bạn cập nhật repository GitHub. Điều này cho phép lặp lại nhanh chóng và đơn giản hóa quy trình triển khai.
- Tên miền tùy chỉnh và HTTPS: Với Pages, bạn có thể gắn tên miền tùy chỉnh cho website, và HTTPS tự động miễn phí được cung cấp cho mọi website, đảm bảo kết nối luôn an toàn.
- Triển khai xem trước: Mỗi khi bạn tạo pull request mới trên repository GitHub đã kết nối, Cloudflare Pages tự động tạo URL xem trước duy nhất để kiểm tra thay đổi trước khi đưa lên chính thức.
Dù bạn là nhà phát triển cá nhân hay thành viên trong một đội ngũ lớn, Cloudflare Pages cung cấp cách dễ dàng, nhanh chóng và an toàn để xuất bản website trực tuyến.
Với những điều trên, cho hệ thống đăng ký người dùng này, tôi đã chọn các trang HTML đơn giản thuần túy mà không sử dụng framework hay công cụ build bổ sung. Cách tiếp cận này đảm bảo sự đơn giản tuyệt đối và mang lại sự linh hoạt để đạt được kết quả mong muốn.
Cloudflare Workers là gì?
Cloudflare Workers là một nền tảng điện toán serverless mang tính đột phá, cho phép các nhà phát triển triển khai code trực tiếp trên mạng lưới rộng lớn của Cloudflare, trải dài hơn 200 thành phố trên toàn thế giới. Về bản chất, nó đảm bảo ứng dụng của bạn chạy gần người dùng cuối nhất có thể, giảm độ trễ và cải thiện trải nghiệm người dùng.
Dưới đây là tổng quan về các tính năng và lợi ích:
- Môi trường thực thi serverless: Cloudflare Workers hoạt động trong môi trường serverless. Điều này có nghĩa là các nhà phát triển không cần quản lý hay duy trì máy chủ. Thay vào đó, họ có thể tập trung vào viết code, và nền tảng xử lý phần còn lại, từ triển khai đến mở rộng.
- Điện toán biên (Edge Computing): Khác với mô hình truyền thống nơi ứng dụng chạy trên một máy chủ hay trung tâm dữ liệu duy nhất, Cloudflare Workers đưa code đến biên của mạng Cloudflare. Điều này đảm bảo ứng dụng chạy gần người dùng hơn, mang lại hiệu suất và tốc độ vượt trội.
- Linh hoạt về ngôn ngữ: Workers sử dụng engine V8 JavaScript – cùng runtime với Chrome – cho phép các nhà phát triển viết code bằng JavaScript. Hơn nữa, nhờ hỗ trợ WebAssembly, các ngôn ngữ khác như Rust, C và C++ cũng có thể được sử dụng.
- Bảo mật: Tận dụng tính bảo mật vốn có của mạng Cloudflare, Workers bảo vệ ứng dụng khỏi nhiều mối đe dọa, bao gồm tấn công DDoS.
Cloudflare Workers cung cấp giải pháp sáng tạo và có khả năng mở rộng cao cho các nhà phát triển muốn nâng cao hiệu suất, độ tin cậy và bảo mật cho ứng dụng của họ.
Trong Cloudflare Pages, Workers được đặt trong thư mục có tên functions. Tôi đã đặt toàn bộ code JavaScript/TypeScript vào không gian này, tận dụng các khả năng toàn diện mà Workers mang lại.
Cloudflare Workers KV là gì?
Cloudflare Workers KV (Key-Value) là kho dữ liệu khóa-giá trị phân tán toàn cầu với tính nhất quán cuối cùng (eventually consistent), cho phép bạn lưu trữ và truy cập dữ liệu từ bên trong các script Cloudflare Workers ở bất kỳ đâu trên thế giới. Nó được thiết kế để giúp mở rộng và đơn giản hóa việc quản lý trạng thái trong môi trường serverless.
Dưới đây là các tính năng và lợi ích chính:
- Phân tán toàn cầu: Cloudflare Workers KV được xây dựng trên mạng Cloudflare, trải dài hơn 300 thành phố trên toàn thế giới. Điều này đảm bảo dữ liệu được lưu trữ và truy cập gần người dùng, giảm độ trễ và cải thiện hiệu suất tổng thể cho ứng dụng.
- Đọc và ghi nhanh: Workers KV cung cấp truy cập dữ liệu với độ trễ thấp, phù hợp cho nhiều loại ứng dụng khác nhau. Mặc dù ghi có thể mất một chút thời gian để lan truyền toàn cầu (thường vài giây), các thao tác đọc thường nhanh, làm cho nó lý tưởng cho các khối lượng công việc cần đọc nhiều.
- Quy mô lớn: Bạn có thể lưu trữ hàng tỷ khóa trong một namespace Workers KV duy nhất, và mỗi khóa có thể chứa giá trị lên đến 25MB.
- Namespace: Namespace KV là các container cho các cặp khóa-giá trị. Chúng cho phép bạn tách biệt các tập dữ liệu khác nhau trong kho Workers KV, đặc biệt hữu ích khi quản lý nhiều ứng dụng hoặc môi trường (chẳng hạn như staging và production).
- Tính nhất quán cuối cùng: Workers KV sử dụng tính nhất quán cuối cùng, nghĩa là các bản cập nhật dữ liệu (ghi) được lan truyền toàn cầu, đảm bảo tính nhất quán theo thời gian – thường chỉ mất vài giây.
Cloudflare Workers KV mang đến giải pháp độc đáo cho việc quản lý trạng thái trong môi trường serverless, cung cấp cho các nhà phát triển hệ thống lưu trữ dữ liệu đáng tin cậy, nhanh chóng và phân tán toàn cầu.
Trong quá trình phát triển hệ thống đăng ký người dùng này, tôi đã thiết kế chiến lược các namespace Workers KV sau:
- USERS: Làm kho lưu trữ chính cho tất cả người dùng. Được thiết kế để xử lý số lượng bản ghi hầu như không giới hạn.
- USERS_LOGIN_HISTORY: Không gian dành riêng cho việc ghi nhật ký hoạt động đăng nhập, cho phép người dùng đánh giá định kỳ tình trạng bảo mật tài khoản.
- USERS_SESSIONS: Namespace này ghi lại chi tiết của người dùng hiện đang đăng nhập. Bao gồm ID duy nhất, thiết bị, vị trí và hơn thế nữa.
- USERS_SESSIONS_MAPPING: Do mô hình nhất quán cuối cùng của Workers KV, có thể xảy ra độ trễ giữa việc ghi vào
USERS_SESSIONSvà kiểm tra sau đó, đặc biệt nếu các thao tác diễn ra ở các vị trí edge khác nhau. Để khắc phục điều này, sau khi xác thực, UID phiên mới được thêm trực tiếp vào USERS_SESSIONS_MAPPING, đảm bảo nó được bao gồm ngay cả trước khi ghi vàoUSERS_SESSIONS. - USERS_TEMP: Tôi sử dụng namespace này làm kho lưu trữ cho các liên kết tạm thời và nội dung khác với thời hạn hết hạn được xác định trước.
Tôi đã tạo ra một cơ sở dữ liệu với dung lượng không giới hạn, tự động mở rộng và tính khả dụng cao. Đây là những tính năng thường chỉ có ở những cơ sở dữ liệu đắt tiền hơn.
Thiết kế dự án
Mục tiêu là tạo ra thứ gì đó đơn giản và hiệu quả mà không phụ thuộc vào thư viện bên thứ ba, và tôi đã thành công. Đây là cấu trúc dự án hoàn chỉnh:
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
32dashboard.htmlĐể hiểu rõ kiến trúc trên, hãy xem phân tích chi tiết:
- framework: Thư mục này chứa code TypeScript nền tảng. Từ mô hình dữ liệu đến mẫu email, mọi thứ đều được đặt ở đây, đảm bảo cách tiếp cận nhất quán trong toàn hệ thống.
- functions: Đây là nơi bạn tìm thấy code TypeScript dành riêng cho Cloudflare Pages Functions, tối ưu hóa các hoạt động backend của website.
- public: Tất cả các tệp HTML tĩnh có thể truy cập công khai đều nằm trong thư mục này, tạo nên giao diện người dùng thấy được.
Nói ngắn gọn, khi bạn truy cập trang login.html, Cloudflare Pages sẽ hoạt động, thực thi code login.ts tương ứng. Sự tương tác động này tiếp tục trên tất cả các trang và các hàm liên kết của chúng.
Với thiết lập này, các hoạt động đa dạng được xử lý liền mạch. Dù là viết lại nội dung, xử lý dữ liệu hay truy xuất dữ liệu thông qua Cloudflare Workers KV, mọi thứ đều được quản lý hiệu quả.
Xây dựng hệ thống đăng ký người dùng
Bắt đầu hành trình, chúng ta sẽ xây dựng hệ thống đăng ký người dùng trước. Đây là chức năng cốt lõi.
- Bước đầu tiên là thiết kế một biểu mẫu HTML đơn giản, đặt nó trong
register.html, và viết một hàm để xử lý dữ liệu được cung cấp:
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>Sau khi biểu mẫu được thiết lập, bước tiếp theo là cải thiện việc gửi dữ liệu bằng JavaScript. Mặc dù việc gửi biểu mẫu truyền thống hoạt động tốt, nhưng tôi đã chọn truyền dữ liệu qua AJAX vì chúng ta đã tích hợp xác thực Bootstrap 5 trong ví dụ.
Đây là ví dụ hoạt động từ bản demo:
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
120})();/register, kích hoạt code functions/register.ts. Cơ chế này cho phép xử lý cả dữ liệu GET và POST. Việc xử lý OPTIONS là khả thi nhưng sẽ không được đề cập trong phần thảo luận này.- Đối với truy cập trang tiêu chuẩn hoặc thực thi GET, chúng ta có thể giới thiệu nhiều chức năng nâng cao như bảo vệ CSRF. Hãy xem ví dụ sau:
1/\*\*
2
3- GET /register
4 \*/
5 export const onRequestGet: PagesFunction = async ({ next }) =>
6 {
7 // Fetch the original page content
8 const response = await next();
9
10 // Prepare our CSRF data
11 const IP = request.headers.get('CF-Connecting-IP');
12 const Country = request.headers.get('CF-IPCountry') || '';
13 const UserAgent = request.headers.get('User-Agent');
14 const expiry = Date.now() + 60000;
15 const CSRF_TOKEN = JSON.stringify({i: IP, c: Country, u: UserAgent, e: expiry});
16 const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
17 const hex = Utils.BytesToHex(new Uint8Array(encryptedData));
18
19 // Rewrite the content and stream it back to the user (async)
20 return new HTMLRewriter()
21 .on("form", {
22 element(form) {
23 // The CSRF input
24 form.append(
25 `<input type="hidden" name="csrf" value="${hex}" />`,
26 { html: true }
27 );
28 },
29 })
30 .transform(response);
31
32 };
33
Trong quy trình này, tính năng HTMLRewriter của Cloudflare Workers được sử dụng để tạo token CSRF duy nhất cho mỗi yêu cầu trang và chèn nó vào biểu mẫu.
Chiến lược CSRF sẽ được mô tả chi tiết trong các phần sau. Hiện tại, hãy chú ý cách một mã CSRF ngẫu nhiên duy nhất được thêm động để tăng cường bảo mật.
- Bây giờ chúng ta chuyển sang giai đoạn thực thi POST, nơi dữ liệu đầu vào được xác thực cẩn thận, lưu trữ an toàn, sau đó gửi email cho người dùng để xác nhận hoặc hướng dẫn thêm. Tôi đã tạo biểu diễn mã giả để cung cấp tổng quan khái niệm:
1/\*\*
2
3- POST /register
4 \*/
5 export const onRequestPost: PagesFunction<{ USERS: KVNamespace; }> = async ({ request, env }) =>
6 {
7 // Validate content type
8 // Validate our CSRF before doing anything
9 // Check for existing user email
10 // Generate a new salt & hash the original password
11 // Store the user with some meta-data
12 // Etc
13 }
14
Trong thiết lập của tôi, sau khi đăng ký thành công, chi tiết người dùng được lưu trong Workers KV như sau:
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}Bạn có thể điều chỉnh định dạng này, thêm hoặc xóa các trường tùy theo nhu cầu. Cần nhấn mạnh rằng tôi sẽ không cung cấp code hoàn chỉnh có thể sao chép. Việc đầu tư vào sự hiểu biết và khám phá thực hành là thiết yếu. Chỉ khi đó bạn mới thực sự phát triển với tư cách nhà phát triển.
Xây dựng hệ thống đăng nhập
Chuyển sang cơ chế đăng nhập cho dự án Cloudflare Pages rất đơn giản. Chúng ta sẽ áp dụng cách tiếp cận tương tự như quy trình đăng ký.
- Giống như cách chúng ta tạo biểu mẫu đăng ký, chúng ta tạo biểu mẫu ngắn gọn cho quy trình đăng nhập, đặt trong
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>- Sau khi thiết lập biểu mẫu, đến lúc xử lý render. Bước tiếp theo là viết code cần thiết. Code này được kích hoạt tự động từ
login.ts:
1/\*\*
2
3- GET /login
4 \*/
5 export const onRequestGet: PagesFunction = async ({ next }) =>
6 {
7 // Fetch the original page content
8 // Prepare our CSRF data
9 // Rewrite the content and stream it back to the user (async)
10 };
11
- Bước cuối cùng là viết code xử lý thực thi POST, nằm trong
login.ts:
1/\*\*
2
3- POST /login
4 \*/
5 export const onRequestPost: PagesFunction<{
6 USERS: KVNamespace;
7 USERS_SESSIONS: KVNamespace;
8 USERS_SESSIONS_MAPPING: KVNamespace;
9 USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) =>
10 {
11 // Validate content type
12 // Validate our CSRF before doing anything
13 // Check for existing user email
14 // Generate a new salt & hash the original password
15 // Compare the passwords
16 // Save session
17 // Update history
18 // Retrieve the current mapping for this user (if it exists)
19 // Add the new session UID to the mapping
20 // Store the updated mapping
21 // Etc
22 };
23
Với 3 bước đơn giản, chúng ta đã xây dựng thành công hệ thống đăng nhập. Không nghi ngờ gì, cơ chế đăng nhập là phần phức tạp nhất, đòi hỏi nhiều thao tác như đã đề cập trong các comment. Tuy nhiên, hãy xem đây không phải là sự phức tạp mà là một thử thách thú vị cần vượt qua!
Xây dựng hệ thống đặt lại mật khẩu
- Tương tự các thành phần trước, chúng ta bắt đầu bằng việc tạo biểu mẫu chuyên dụng để khởi tạo đặt lại mật khẩu:
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
12</form>Khái niệm cơ bản rất đơn giản: khi email của người dùng được nhập và được nhận dạng trong hệ thống, một liên kết đặt lại mật khẩu dùng một lần sẽ được tạo và gửi đi. Để tăng cường bảo mật, hãy đảm bảo liên kết này chỉ hoạt động trong thời gian ngắn – lý tưởng không quá 2 giờ.
- Bắt đầu viết code xử lý thực thi GET, tích hợp vào
forgot-password.ts:
1/\*\*
2
3- GET /forgot-password
4 \*/
5 export const onRequestGet: PagesFunction = async ({ next }) =>
6 {
7 // Fetch the original page content
8 // Prepare our CSRF data
9 // Rewrite the content and stream it back to the user (async)
10 };
11
- Viết code chịu trách nhiệm thực thi POST, tích hợp vào cùng file
forgot-password.ts:
1/\*\*
2
3- POST /forgot-password
4 \*/
5 export const onRequestPost: PagesFunction<{
6 USERS: KVNamespace;
7 USERS_TEMP: KVNamespace;
8 }> = async ({ request, env }) =>
9 {
10 // Validate content type
11 // Validate our CSRF before doing anything
12 // Check for existing user email
13 // Generate a unique string to hash
14 // Hash the string
15 // Save to KV, expiration 2 hours
16 // Send email to user
17 // Etc
18 };
19
Bây giờ, việc hoàn thành câu đố lập trình là của bạn. Theo hướng dẫn trong các comment, quy trình sẽ khá dễ quản lý. Khi hoàn tất, bạn sẽ có hệ thống đặt lại mật khẩu hoạt động đầy đủ.
Lưu ý quan trọng: Không hiển thị thông báo như “Email đó không tồn tại”. Thông tin như vậy là mỏ vàng cho hacker, mở đường cho các cuộc tấn công brute force tiềm tàng. Thay vào đó, hãy áp dụng cách tiếp cận mơ hồ hơn nhưng thân thiện với người dùng: “Nếu email được đăng ký trong hệ thống của chúng tôi, bạn sẽ nhận được liên kết đặt lại.”
Demo đăng ký người dùng
Khám phá bản demo trực tiếp của quy trình đăng ký người dùng mà tôi đã xây dựng: https://members.mecanik.dev/
Các tính năng bao gồm:
- Đăng ký, đăng nhập và đặt lại mật khẩu
- Truy cập hồ sơ
- Quản lý bảo mật và phiên hoạt động - ví dụ, nếu bạn đăng nhập từ điện thoại, bạn có thể ngắt kết nối phiên đó.
- Xem trước phần mềm miễn phí và sản phẩm premium sắp ra mắt.
Hiện tại tôi cũng đang làm việc trên các dự án khác, nhưng sẽ tiếp tục cải thiện nền tảng này. Hãy thoải mái thử nghiệm - vui lòng sử dụng email thật để không ảnh hưởng đến uy tín SendGrid của tôi.
Rất hoan nghênh phản hồi và báo cáo lỗi. Nếu có bất kỳ điều gì, đừng ngần ngại liên hệ!
Câu hỏi và trả lời
Không nghi ngờ gì, bảo mật là ưu tiên hàng đầu. Bạn có thể thắc mắc về độ vững chắc của hệ thống trước các cuộc tấn công tiềm tàng và rò rỉ dữ liệu.
Hãy lưu ý rằng Cloudflare Pages hoạt động trên nền tảng Cloudflare, cung cấp quyền truy cập vào Web Application Firewall (WAF) tiên tiến và bộ công cụ bảo mật.
CSRF thực sự hoạt động như thế nào?
Bạn có thể đã nhận thấy rằng mỗi lần tải trang, một token CSRF được tạo động và chèn vào biểu mẫu. Đây là cách cơ chế này đảm bảo bảo mật cho các yêu cầu:
Các thành phần token CSRF:
- IP: Ghi lại địa chỉ IP hiện tại của người dùng.
- Country: Xác định vị trí hiện tại của người dùng.
- UserAgent: Ghi nhận thông tin trình duyệt của người dùng.
- expiry: Đặt bộ đếm thời gian bằng cách thêm 1 phút vào thời gian hiện tại.
Dữ liệu này được kết hợp ở định dạng JSON: {i, c, u, e}. Sau đó được mã hóa và chuyển đổi sang Hex:
1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));Về hàm mã hóa:
- Mã hóa dữ liệu sử dụng mật khẩu tùy chỉnh, tạo khóa mật khẩu và sau đó tạo khóa AES dẫn xuất.
- Khóa AES này mã hóa dữ liệu, với salt và IV (Vector Khởi tạo) được tạo ngay tại chỗ.
- Kết quả mã hóa là ArrayBuffer đóng gói số lần lặp, salt, IV và dữ liệu đã mã hóa.
Tóm lại, quy trình mã hóa này tuân thủ các tiêu chuẩn ngành, đảm bảo dữ liệu mã hóa không thể giải mã và được bảo vệ khỏi sự giả mạo.
Xác thực token 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{
7return new Response(JSON.stringify({
8result: null,
9success: false,
10// An ambigous message; don't tell the hacker what's missing.
11error: { code: 1001, message: "Invalid CSRF Token. Please refresh the page and try again." }
12}),
13{
14status: 403,
15headers: { '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{
26return new Response(JSON.stringify({
27result: null,
28success: false,
29// An ambigous message; don't tell the hacker what's missing.
30error: { code: 1002, message: "Invalid CSRF Token. Please refresh the page and try again." }
31}),
32{
33status: 403,
34headers: { 'content-type': 'application/json;charset=UTF-8'
35}
36});
37}Quy trình xác thực kiểm tra nghiêm ngặt token CSRF, đảm bảo:
- Xuất phát từ cùng địa chỉ IP.
- Được gửi từ cùng quốc gia.
- Được gửi từ cùng trình duyệt.
- Được gửi trong khoảng thời gian hiệu lực.
- Nếu bất kỳ kiểm tra nào thất bại, hệ thống xác định token CSRF là không hợp lệ, cung cấp bảo vệ vững chắc chống lại các mối đe dọa tiềm tàng.
Nếu bất kỳ kiểm tra nào thất bại, hệ thống xác định token CSRF là không hợp lệ, cung cấp bảo vệ vững chắc chống lại các mối đe dọa tiềm tàng.
Băm/Mã hóa mật khẩu
Để băm/mã hóa mật khẩu, tôi đã sử dụng PBKDF2 (Password-Based Key Derivation Function 2) với thuật toán băm SHA-256 .
Phương pháp này sử dụng “salt” giả ngẫu nhiên, an toàn mật mã, duy nhất cho mỗi mật khẩu, đảm bảo bảo mật tăng cường. Cách tiếp cận này cung cấp bảo vệ chống lại các cuộc tấn công rainbow table và brute force.
Phiên bản băm của mật khẩu được lưu trữ, không phải văn bản thuần, càng củng cố tính toàn vẹn dữ liệu.
Tại sao không dùng JWT Token?
JWT (JSON Web Tokens) là phương pháp phổ biến để xử lý xác thực người dùng và truyền thông tin giữa các bên một cách gọn nhẹ và an toàn cho URL.
Mặc dù có nhiều ưu điểm, vẫn có lý do để không sử dụng chúng trong một số tình huống nhất định, chẳng hạn như hệ thống đăng ký người dùng hoàn chỉnh. Hãy cùng tìm hiểu:
- Tính không trạng thái và thu hồi: Một đặc điểm nổi bật của JWT là tính không trạng thái. Tuy nhiên, đặc tính này có thể gây vấn đề trong hệ thống quản lý người dùng. Ví dụ, nếu token JWT của người dùng bị đánh cắp hoặc xâm phạm, không có cách đơn giản để thu hồi token đó mà không duy trì danh sách đen token, điều này vô hiệu hóa mục đích của tính không trạng thái.
- Kích thước: Khi bạn thêm nhiều dữ liệu vào JWT, kích thước của nó tăng lên. Nếu bạn có hệ thống đăng ký người dùng toàn diện cần lưu thêm dữ liệu liên quan đến người dùng trong token, điều này có thể dẫn đến header HTTP lớn hơn và độ trễ tăng.
- Bảo mật lưu trữ: Để lưu trữ JWT phía client, các vị trí phổ biến bao gồm local storage hoặc cookie. Local storage dễ bị tổn thương trước tấn công XSS, còn cookie mặc dù được bảo vệ tốt hơn nhưng có thể mở đường cho tấn công CSRF nếu không xử lý đúng.
- Xử lý hết hạn: Quản lý thời hạn JWT có thể phức tạp. Token ngắn hạn giảm rủi ro nhưng cần cơ chế làm mới, thêm phức tạp vào luồng xác thực.
- Không có thu hồi tích hợp: Như đã đề cập, nếu token bị xâm phạm, không có cơ chế sẵn có để thu hồi hoặc vô hiệu hóa nó cho đến khi hết hạn. Đây có thể là rủi ro bảo mật đáng kể nếu token có thời hạn dài.
- Phức tạp cho nhà phát triển: Đối với những người chưa quen với JWT, có đường cong học tập trong việc hiểu cách tạo, xác thực và sử dụng đúng. Sự phức tạp này có thể dẫn đến sai sót và lỗ hổng nếu không được hiểu và triển khai kỹ lưỡng.
Khi khám phá nhiều hệ thống khác nhau và cách chúng xử lý dữ liệu người dùng, tôi phát hiện rằng một số công ty lưu lượng thông tin khổng lồ, bao gồm dữ liệu nhạy cảm như vai trò người dùng, trong token JWT.
Để rõ ràng, tôi không có ý phê phán hay hạ thấp các cách tiếp cận khác. Tuy nhiên, từ góc độ chuyên nghiệp về bảo mật, việc bao gồm thông tin quan trọng trực tiếp trong token gây ra lo ngại. Có rủi ro cố hữu khi tiết lộ nhiều dữ liệu hơn mức cần thiết, đặc biệt khi dữ liệu đó có thể bị khai thác để suy ra quyền hạn người dùng hoặc các chức năng khác.
Dựa trên những lo ngại này, tôi đã đưa ra quyết định có ý thức rời khỏi phương pháp này. Thay vào đó, tôi chuyển sang kết hợp xác thực phía máy chủ đã được kiểm chứng và xử lý cookie an toàn. Cách tiếp cận này, theo ý kiến tôi, tạo sự cân bằng giữa trải nghiệm người dùng và bảo mật vững chắc, đảm bảo dữ liệu nhạy cảm được bảo vệ và hệ thống vẫn linh hoạt trước các lỗ hổng tiềm tàng.
Gửi email đáng tin cậy
Dựa trên sự phổ biến rộng rãi và hiểu biết sâu sắc về các sắc thái của nó, tôi đã chọn SendGrid với template động. Để tìm hiểu thêm, hãy tham khảo tài liệu API .
Mặc dù tích hợp có thể phức tạp, nhưng tất cả đều là phần của thử thách đáng giá. Đây là ví dụ:
1const SENDGRID_API_URL = 'https://api.sendgrid.com/v3/mail/send';
2const SENDGRID_API_KEY = 'YOUR_SENDGRID_API_KEY'; // Replace with your actual API key
3
4interface EmailDetails {
5to: string;
6from: string;
7subject: string;
8content: string;
9}
10
11async function sendEmail(details: EmailDetails): Promise<void> {
12const data = {
13personalizations: [{
14to: [{
15email: details.to
16}]
17}],
18from: {
19email: details.from
20},
21subject: details.subject,
22content: [{
23type: 'text/plain',
24value: 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('Email sent successfully!');
40 } else {
41 const responseBody = await response.json();
42 console.error('Failed to send email:', responseBody.errors);
43 }
44 } catch (error) {
45 console.error('Error sending email:', error);
46 }
47
48}
49
50// Usage example:
51sendEmail({
52to: '[email protected]',
53from: '[email protected]',
54subject: 'Hello from SendGrid!',
55content: 'This is a test email sent using the SendGrid API.'
56});Làm thế nào để tăng cường bảo mật hơn nữa?
Mặc dù Cloudflare Workers KV mã hóa dữ liệu lưu trữ, bạn có thể thêm lớp mã hóa bổ sung cho dữ liệu người dùng.
Hoặc bạn muốn gây khó khăn cho kẻ tấn công tiềm tàng? Hãy cân nhắc triển khai các trường input giả trong biểu mẫu để gây nhầm lẫn cho cả bot lẫn hacker:
1return new HTMLRewriter()
2.on("form",
3{
4element(form)
5{
6// Dummy inputs... just to give the hacker more headache and confuse him :)
7const 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
19.transform(response);Đoạn mã này tạo động từ 1 đến 5 trường input có tên ngẫu nhiên mỗi khi tải trang.
Khả năng là gần như vô hạn. Tất cả những gì bạn cần là trí tưởng tượng.
Kết luận
Vậy là xong! Tôi chân thành hy vọng bài viết này mang lại nhiều thông tin sâu sắc và hấp dẫn cho bạn. Dù bạn là nhà phát triển kỳ cựu hay mới bước chân vào thế giới website đám mây có khả năng mở rộng, việc chia sẻ kiến thức là thiết yếu cho sự phát triển cộng đồng.
Nếu bạn thấy bài viết này có giá trị, tôi sẽ rất biết ơn nếu bạn chia sẻ với các đồng nghiệp nhà phát triển hoặc những ai quan tâm đến chủ đề này. Mỗi lần chia sẻ đều mở rộng sự hiểu biết chung của chúng ta.
Phản hồi và câu hỏi của bạn luôn được đánh giá cao. Đừng ngần ngại để lại bình luận hoặc liên hệ – hãy cùng duy trì cuộc trò chuyện. Lập trình an toàn!
Bình luận