في المشهد المتطور لتطوير الويب، فإن التطبيقات بدون خادم تترك بصماتها بشكل ثابت. إن مزاياها التي لا يمكن إنكارها، مثل قابلية التوسع التي لا مثيل لها والأداء القوي، تجعلها متميزة. يكمن جمال الخدمة بدون خادم في وعدها: تقديم حلول قوية لا تتوسع بسهولة فحسب، بل تكون أيضًا صديقة للميزانية.

هل حلمت يومًا بإطلاق فكرة عمل، ولكن تم ردعك من خلال الجوانب الفنية لإنشاء نظام تسجيل المستخدم وتسجيل الدخول؟ على الرغم من وجود منصات مثل Netlify وقواعد البيانات السحابية المختلفة، إلا أنها لا يمكن مقارنتها بالإمكانيات التي توفرها Cloudflare. يمكن للعديد من البدائل تصعيد التكاليف عند التوسع، ولكن مع صفحات Cloudflare، القصة مختلفة.

لقد كنت أعمل بشكل متقطع على هذا المشروع والعرض التوضيحي خلال الأشهر القليلة الماضية، وأقوم بالتوفيق بينه وبين التزاماتي الأخرى. أعتذر عن الانتظار، خاصة لأولئك الذين كانوا ينتظرون بفارغ الصبر هذا النظام.

أطلق العنان لقوة صفحات Cloudflare

  • قابلية التوسع السلسة: إدارة تسجيلات المستخدمين غير المحدودة دون أي عوائق.
  • فعالية التكلفة: قل وداعًا للنفقات العامة غير المتوقعة؛ استمتع بأسعار متسقة.
  • سرعات مذهلة: استمتع بأداء لم يسبق له مثيل.
  • الأمان القوي: تظل بيانات المستخدمين محمية وآمنة.

ما سوف نستخدمه

  • صفحات Cloudflare
  • وظائف صفحات Cloudflare
  • عمال Cloudflare KV

ما هي صفحات Cloudflare؟

Cloudflare Pages عبارة عن نظام أساسي حديث وسهل الاستخدام للمطورين لإنشاء مواقعهم الإلكترونية ونشرها واستضافتها. إنه يوفر تكاملًا سلسًا مع GitHub، مما يعني أنه يمكنك ببساطة دفع التعليمات البرمجية الخاصة بك إلى GitHub وستتولى Cloudflare Pages الباقي - الإنشاء والنشر وحتى التحديثات.

وإليك كيف يعمل:

  1. سير العمل المتكامل: تم إنشاء صفحات Cloudflare حول سير عمل git. بمجرد توصيل مستودع GitHub الخاص بك بصفحات Cloudflare، فإنه يبدأ في إنشاء موقعك ونشره في كل مرة تدفع فيها إلى الفرع المحدد.
  2. JAMstack Optimized: تدعم Cloudflare Pages مبادئ JAMstack، مما يعني أنه يمكنك إنشاء موقعك باستخدام منشئ الموقع الثابت المفضل لديك أو إطار عمل JavaScript، بما في ذلك، على سبيل المثال لا الحصر، Jekyll وHugo وNext.js وReact.
  3. تسليم سريع وآمن: مدعومًا بشبكة Cloudflare الموزعة عالميًا، يضمن Pages أن موقعك متاح وسريع، بغض النظر عن مكان جمهورك. كما أن ميزات الأمان المتأصلة في Cloudflare تحافظ على حماية موقعك من التهديدات.
  4. النشر المستمر: تقوم Cloudflare Pages بإنشاء موقعك ونشره تلقائيًا في كل مرة تقوم فيها بإجراء تحديثات على مستودع GitHub الخاص بك. يتيح لك هذا إمكانية التكرار بسرعة ويجعل عملية النشر سهلة للغاية.
  5. النطاق المخصص وHTTPS: باستخدام الصفحات، يمكنك ربط نطاق مخصص بموقعك، وهو يوفر HTTPS تلقائيًا وتلقائيًا على جميع المواقع لضمان أن الاتصال آمن دائمًا.
  6. معاينة عمليات النشر: عندما تقوم بإنشاء طلب سحب جديد في مستودع GitHub المرتبط الخاص بك، تقوم صفحات Cloudflare تلقائيًا بإنشاء عنوان URL فريد للمعاينة، مما يسمح لك برؤية تغييراتك قبل البث المباشر.

سواء كنت مطورًا منفردًا أو جزءًا من فريق كبير، توفر Cloudflare Pages طريقة سهلة وسريعة وآمنة لنشر مواقع الويب الخاصة بك على الإنترنت.

في ضوء ما سبق، بالنسبة لنظام تسجيل المستخدم هذا، اخترت صفحات HTML نقية ومباشرة، متجنبًا أي أطر عمل إضافية أو أدوات بناء. يضمن هذا النهج بساطة لا مثيل لها ويمنح المرونة لتحقيق أي نتيجة مرغوبة.

ما هو عمال Cloudflare؟

Cloudflare Workers عبارة عن منصة حوسبة مبتكرة بدون خادم تتيح للمطورين نشر التعليمات البرمجية الخاصة بهم مباشرةً على شبكة Cloudflare الواسعة، والتي تمتد لأكثر من 200 مدينة حول العالم. بشكل أساسي، فهو يمكّن التطبيقات من التشغيل في أقرب مكان ممكن من المستخدمين النهائيين، وبالتالي تقليل زمن الوصول وتحسين تجربة المستخدم.

وفيما يلي نظرة عامة على ميزاته وفوائده:

  1. بيئة التنفيذ بدون خادم: يعمل عمال Cloudflare في بيئة بدون خادم، مما يعني أنه لا يتعين على المطورين إدارة أو صيانة أي خوادم. وبدلاً من ذلك، يمكنهم التركيز على كتابة التعليمات البرمجية الخاصة بهم، بينما تعتني المنصة بالباقي، بدءًا من التوزيع وحتى التوسع.
  2. حوسبة الحافة: على عكس النماذج التقليدية حيث تعمل التطبيقات على خادم واحد أو مركز بيانات واحد، يقوم Cloudflare Workers بإحضار التعليمات البرمجية الخاصة بك إلى حافة شبكة Cloudflare. وهذا يضمن تشغيل التطبيق الخاص بك بالقرب من المستخدم، مما يوفر أداء وسرعة أفضل.
  3. مرونة اللغة: يستخدم العمال محرك V8 JavaScript، وهو نفس وقت التشغيل الذي يستخدمه Chrome، والذي يسمح للمطورين بكتابة التعليمات البرمجية في JavaScript. علاوة على ذلك، بفضل دعم WebAssembly، يمكن أيضًا استخدام لغات أخرى مثل Rust وC وC++.
  4. الأمان: من خلال الاستفادة من الأمان المتأصل لشبكة Cloudflare، يساعد العاملون في حماية التطبيقات من مجموعة متنوعة من التهديدات مثل هجمات DDoS.

يوفر Cloudflare Workers حلاً مبتكرًا وقابلاً للتطوير بدرجة كبيرة للمطورين الذين يتطلعون إلى تحسين أداء تطبيقاتهم وموثوقيتها وأمانها.

داخل صفحات Cloudflare، يتم وضع العمال في دليل يسمى “الوظائف”. لقد قمت بوضع كافة أكواد JavaScript/TypeScript الخاصة بي في هذه المساحة، مع الاستفادة من الإمكانات الشاملة التي يقدمها العمال.

ما هو Cloudflare Workers KV؟

Cloudflare Workers KV (Key-Value) هو نظام تخزين ذو قيمة أساسية موزع عالميًا ومتسق في نهاية المطاف ويسمح لك لتخزين البيانات والوصول إليها من أي مكان داخل البرامج النصية الخاصة بـ Cloudflare Workers. لقد تم تصميمه لمساعدتك على توسيع نطاق إدارة الحالة وتبسيطها في البيئات التي لا تحتوي على خادم.

فيما يلي ميزاته وفوائده الرئيسية:

  1. التوزيع العالمي: تم إنشاء Cloudflare Workers KV استنادًا إلى شبكة Cloudflare، التي تمتد لأكثر من 300 مدينة حول العالم. وهذا يضمن تخزين بياناتك والوصول إليها بالقرب من المستخدمين، مما يقلل من زمن الوصول ويحسن الأداء العام لتطبيقاتك.
  2. ** عمليات القراءة والكتابة السريعة **: يوفر Workers KV وصولاً إلى البيانات بزمن وصول منخفض مناسب لمجموعة متنوعة من التطبيقات. على الرغم من أن عمليات الكتابة تستغرق وقتًا أطول قليلاً للنشر عالميًا (عادةً في غضون ثوانٍ قليلة)، إلا أن عمليات القراءة تكون سريعة عادةً، مما يجعلها مثالية لأحمال العمل كثيفة القراءة.
  3. الحجم الكبير: يمكنك تخزين مليارات المفاتيح في مساحة اسم Workers KV واحدة ويمكن أن يحمل كل مفتاح قيمة كبيرة تصل إلى 25 ميجابايت.
  4. مساحات الأسماء: مساحات أسماء KV هي حاويات لأزواج القيمة الرئيسية. إنها تسمح لك بفصل مجموعات مختلفة من البيانات داخل متجر Workers KV الخاص بك، والذي يمكن أن يكون مفيدًا بشكل خاص عند إدارة تطبيقات أو بيئات متعددة (مثل التدريج والإنتاج).
  5. الاتساق النهائي: يستخدم Workers KV الاتساق النهائي. وهذا يعني أن تحديثات بياناتك (عمليات الكتابة) ستنتشر عالميًا وستصبح متسقة مع مرور الوقت، وهو ما يستغرق عادةً بضع ثوانٍ.

يقدم Cloudflare Workers KV حلاً فريدًا لإدارة الحالة في البيئات التي لا تحتوي على خادم، مما يوفر للمطورين نظام تخزين بيانات موثوقًا وسريعًا وموزعًا عالميًا.

أثناء تطوير نظام تسجيل المستخدم هذا، قمت بتصميم مساحات أسماء Workers KV التالية بشكل استراتيجي:

  • USERS: هذا بمثابة التخزين الأساسي لجميع المستخدمين. لقد تم تصميمه للتعامل مع عدد لا حصر له من السجلات.
  • USERS_LOGIN_HISTORY: مساحة مخصصة لتسجيل أنشطة تسجيل الدخول، وتمكين المستخدمين من تقييم البصمة الأمنية لحساباتهم بشكل دوري.
  • USERS_SESSIONS: تلتقط مساحة الاسم هذه تفاصيل حول المستخدم الذي قام بتسجيل الدخول حاليًا، بما في ذلك المعرفات الفريدة والأجهزة والمواقع والمزيد.
  • 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: ستجد هنا رمز TypeScript المصمم خصيصًا لوظائف Cloudflare Pages، مما يؤدي إلى تبسيط عمليات الواجهة الخلفية للموقع.
  • public: جميع ملفات HTML الثابتة التي يمكن الوصول إليها بشكل عام موجودة في هذا المجلد، مما يشكل الواجهة المرئية للمستخدمين.

ببساطة، عندما تنتقل إلى صفحة login.html، تبدأ صفحات Cloudflare في العمل، وتنفذ رمز 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})();

بمجرد النقر على “إنشاء حساب”، يتم نشر البيانات إلى /register عبر AJAX، مما يؤدي إلى تشغيل رمز functions/register.ts. تسمح هذه الآلية بمعالجة بيانات GET وPOST. على الرغم من أن إدارة الخيارات أمر ممكن، إلا أننا سنضعه جانبًا في هذه المناقشة.

  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	// Validate content type
 7	// Validate our CSRF before doing anything
 8	// Check for existing user email
 9	// Generate a new salt & hash the original password
10	// Store the user with some meta-data
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	// إلخ
21};

من خلال هذه الخطوات الثلاث المبسطة، قمنا بتصميم نظام تسجيل الدخول بسلاسة. لا يمكن إنكار أن آلية تسجيل الدخول هي الجزء الأكثر تعقيدًا، مما يستلزم العديد من العمليات كما هو موضح. ومع ذلك، لا تنظروا إلى هذا باعتباره تعقيدًا، بل باعتباره تحديًا منشطًا يجب التغلب عليه!

بناء نظام إعادة تعيين كلمة مرور المستخدم

  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>

المفهوم الأساسي واضح ومباشر: عند إدخال البريد الإلكتروني للمستخدم، إذا تم التعرف عليه داخل نظامنا، يتم إنشاء رابط فريد لإعادة تعيين كلمة المرور وإرساله. لتعزيز الأمان، تأكد من أن هذا الارتباط يظل نشطًا لمدة قصيرة، ومن الأفضل ألا تزيد عن ساعتين.

  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	// إلخ
17};

الأمر متروك لك الآن لإكمال لغز الترميز. باتباع الإرشادات الواردة في تعليقاتي، يجب أن تجد العملية سهلة الإدارة تمامًا. بمجرد الانتهاء، سوف تكون مجهزًا بنظام إعادة تعيين كلمة المرور التشغيلي بالكامل.

ملاحظة تحذيرية: امتنع عن عرض رسائل مثل “لا يوجد مثل هذا البريد الإلكتروني”. مثل هذه المؤشرات هي بمثابة مناجم ذهب للمتسللين، مما يمهد الطريق لهجمات القوة الغاشمة المحتملة. بدلاً من ذلك، اعتمد نهجاً أكثر غموضاً وسهل الاستخدام: “إذا تم تسجيل البريد الإلكتروني في نظامنا، فسوف تتلقى رابط إعادة التعيين”.

عرض تسجيل المستخدم

استكشف عرضًا توضيحيًا مباشرًا لعملية تسجيل المستخدم التي قمت بإنشائها: https://members.mecanik.dev/

وتشمل الميزات:

  • إمكانيات التسجيل وتسجيل الدخول وإعادة تعيين كلمة المرور
  • الوصول إلى ملفك الشخصي
  • إدارة إعدادات الأمان والجلسات النشطة - يمكنك قطع الاتصال على سبيل المثال إذا قمت بتسجيل الدخول من هاتفك المحمول.
  • قم بمعاينة البرامج المجانية وابحث عن العروض المتميزة القادمة.

أقوم حاليًا بالتوفيق بين مشاريع أخرى، لكنني سأستمر في تحسين هذا النظام الأساسي. لا تتردد في اختباره - فقط تأكد من استخدام رسائل البريد الإلكتروني الأصلية لتجنب التأثير على سمعة SendGrid الخاصة بي.

وأنا أرحب بأي ردود فعل أو القضايا المبلغ عنها. يرجى التواصل إذا كان لديك أي!

أسئلة وأجوبة

مما لا شك فيه أن الأمن هو قمة العقل. قد تشكك في قوة النظام ضد الهجمات المحتملة وانتهاكات البيانات.

ضع في اعتبارك أن Cloudflare Pages تعمل على منصة Cloudflare، مما يتيح لك الوصول إلى جدار حماية تطبيقات الويب المتقدم (WAF) ومجموعة من أدوات الأمان.

كيف يعمل CSRF فعليًا؟

ربما لاحظت الإنشاء الديناميكي وحقن رمز CSRF في النموذج عند كل عملية تحميل. وإليك كيف تضمن هذه الآلية أمان الطلبات:

مكونات رمز CSRF:

  • IP: يلتقط عنوان IP الحالي للمستخدم.
  • Country: يحدد الموقع الحالي للمستخدم.
  • UserAgent: يسجل تفاصيل متصفح المستخدم.
  • expiry: يضبط مؤقتًا بإضافة دقيقة واحدة إلى الوقت الحالي.

يتم تجميع هذه البيانات بتنسيق JSON: {i, c, u, e}، والذي يتم تشفيره لاحقًا وتحويله إلى Hex:

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 الذي يغلف عدد التكرارات والملح و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 خوارزمية التجزئة.

تستخدم هذه الطريقة “ملحًا” عشوائيًا زائفًا فريدًا وآمنًا للتشفير لكل كلمة مرور، مما يضمن تعزيز الأمان. يوفر هذا الأسلوب الحماية ضد طاولة قوس قزح وهجمات القوة الغاشمة.

يتم تخزين النسخة المجزأة من كلمة المرور، وليس النص العادي، مما يعزز سلامة البيانات.

لماذا لا يتم استخدام رمز JWT؟

تعد JWT (JSON Web Tokens) طريقة شائعة للتعامل مع مصادقة المستخدم ونقل المعلومات بين الأطراف بطريقة مدمجة وآمنة على عنوان URL.

على الرغم من أنها تتمتع بالعديد من المزايا، إلا أن هناك أسباب قد تجعل المرء يختار عدم استخدامها في سياقات محددة، مثل نظام تسجيل المستخدم الكامل. وإليك نظرة أعمق:

  1. انعدام الجنسية وإلغاء الجنسية: إحدى السمات المميزة لفرق الحرب المشتركة هي انعدام الجنسية. ومع ذلك، فإن هذه الطبيعة بالذات يمكن أن تكون مشكلة بالنسبة لأنظمة إدارة المستخدم. على سبيل المثال، إذا تمت سرقة رمز JWT الخاص بالمستخدم أو تعرض للاختراق، فلا توجد طريقة مباشرة لإلغاء هذا الرمز المميز ما لم تحتفظ بقائمة سوداء من الرموز المميزة، مما يتعارض مع غرض انعدام الجنسية.
  2. الحجم: عندما تضيف المزيد من البيانات إلى JWT، يزداد حجمها. إذا كان لديك نظام تسجيل مستخدم شامل حيث قد تحتاج إلى تخزين المزيد من البيانات المتعلقة بالمستخدم في الرمز المميز، فقد يؤدي ذلك إلى رؤوس HTTP أكبر وزيادة زمن الوصول.
  3. أمان التخزين: بالنسبة إلى تخزين JWTs من جانب العميل، تتضمن مواقع التخزين الشائعة التخزين المحلي أو ملفات تعريف الارتباط. التخزين المحلي عرضة لهجمات XSS، وبينما يمكن تأمين ملفات تعريف الارتباط إلى حد كبير، فإنها تفتح طرقًا لهجمات CSRF إذا لم يتم التعامل معها بشكل صحيح.
  4. التعامل مع انتهاء الصلاحية: يمكن أن تكون إدارة انتهاء صلاحية JWTs معقدة. في حين أن الرموز قصيرة العمر تقلل من المخاطر، إلا أنها تتطلب آلية تحديث، مما يؤدي إلى مزيد من التعقيد في تدفق المصادقة.
  5. لا يوجد إبطال مضمن: كما ذكرنا سابقًا، إذا تم اختراق رمز مميز، فلا توجد آلية متأصلة لإبطاله أو إبطاله حتى تنتهي صلاحيته. يمكن أن يشكل هذا خطرًا أمنيًا كبيرًا إذا كان الرمز المميز ذا عمر طويل.
  6. التعقيد للمطورين: بالنسبة لأولئك الذين ليسوا على دراية بـ JWTs، هناك منحنى تعليمي يتعلق بفهم كيفية إنشائها والتحقق من صحتها واستخدامها بشكل صحيح. يمكن أن يؤدي هذا التعقيد إلى حدوث أخطاء ونقاط ضعف إذا لم يتم فهمه وتنفيذه بشكل كامل.

في استكشافي للأنظمة المختلفة وتعاملها مع بيانات المستخدم، وجدت أن بعض الشركات تخزن وفرة من المعلومات داخل رموز JWT الخاصة بها، بما في ذلك التفاصيل الحساسة مثل أدوار المستخدم، من بين أمور أخرى.

ولكي أكون واضحًا، فإن نيتي ليست انتقاد الأساليب الأخرى أو الإضرار بها. ومع ذلك، استنادًا إلى موقفي المهني بشأن الأمن، فإن تضمين مثل هذه المعلومات المهمة مباشرة داخل الرموز المميزة يثير المخاوف. هناك خطر متأصل يرتبط بالكشف عن بيانات أكثر من اللازم، خاصة عندما يكون من المحتمل استغلال تلك البيانات لاستنتاج امتيازات المستخدم أو وظائف أخرى.

ونظراً لهذه التحفظات، اتخذت قراراً واعياً بالابتعاد عن هذا الأسلوب. وبدلاً من ذلك، انجذبت نحو المصادقة المجربة والحقيقية من جانب الخادم جنبًا إلى جنب مع المعالجة الآمنة لملفات تعريف الارتباط. في رأيي، يحقق هذا النهج توازنًا بين تجربة المستخدم والأمان القوي، مما يضمن بقاء البيانات الحساسة محمية وبقاء النظام مرنًا ضد نقاط الضعف المحتملة.

إرسال رسائل البريد الإلكتروني بشكل موثوق

لقد اخترت SendGrid باستخدام القوالب الديناميكية نظرًا لاعتمادها على نطاق واسع ومعرفتي العميقة بالفروق الدقيقة فيها. للتعمق أكثر، راجع وثائق واجهة برمجة التطبيقات .

في حين أن تكاملها قد يشكل بعض التعقيدات، إلا أن ذلك كله جزء من التحدي المجزي. هنا مثال لك:

 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 مدخلات مسماة عشوائيًا مع كل تحميل للصفحة.

القدرات غير محدودة تقريبًا، ما عليك سوى استخدام خيالك.

تغليف

ويوجد لدينا ذلك! أتمنى مخلصًا أن تكون هذه القطعة مفيدة وجذابة بالنسبة لك. سواء كنت مطورًا متمرسًا أو مجرد الغوص في عالم مواقع الويب السحابية القابلة للتطوير، فإن مشاركة المعرفة أمر حيوي لنمو مجتمعنا.

إذا وجدت هذه المقالة ذات قيمة، فيرجى التفكير في تمريرها إلى زملائك المطورين أو المهتمين بالموضوع. كل مشاركة توسع فهمنا الجماعي.

هي موضع تقدير كبير ملاحظاتك والأسئلة. لا تتردد في ترك تعليق أو التواصل معنا – فلنواصل المحادثة. الترميز الآمن!