A webfejlesztés változó tájképében a szerver nélküli alkalmazások egyre inkább kiveszik a részüket. Tagadhatatlan előnyeik, mint a páratlan skálázhatóság és a robusztus teljesítmény, kiemelkedővé teszik őket. A serverless szépsége az ígéretében rejlik: olyan hatékony megoldásokat nyújtani, amelyek nemcsak könnyedén skálázhatók, hanem költségbarátok is.

Álmodott már arról, hogy üzleti ötletet indítson, de elriasztották a felhasználói regisztráció és bejelentkezési rendszer létrehozásának technikai kihívásai? Bár léteznek olyan platformok, mint a Netlify és különböző felhőalapú adatbázisok, nem hasonlíthatók össze a Cloudflare által kínált képességekkel. Sok alternatíva a skálázáskor növelheti a költségeket, de a Cloudflare Pages esetében más a helyzet.

Az elmúlt néhány hónapban szórványosan dolgoztam ezen a projekten és demón, más kötelezettségeim mellett zsonglőrködve vele. Elnézést a várakozásért, különösen azoktól, akik türelmetlenül várták ezt a rendszert.

Szabadítsa fel a Cloudflare Pages erejét

  • Zökkenőmentes skálázhatóság: Korlátlan számú felhasználói regisztráció kezelése akadálytalanul.
  • Költséghatékonyság: Búcsúzzon el a váratlan rezsiköltségektől; élvezze a következetes árazást.
  • Villámgyors sebesség: Tapasztaljon meg eddig nem látott teljesítményt.
  • Szilárd biztonság: Felhasználói adatai védettek és biztonságban maradnak.

Amit használni fogunk

  • Cloudflare Pages
  • Cloudflare Pages Functions
  • Cloudflare Workers KV

Mi az a Cloudflare Pages?

A Cloudflare Pages egy modern, felhasználóbarát platform fejlesztők számára webhelyek építéséhez, telepítéséhez és üzemeltetéséhez. Zökkenőmentes integrációt kínál a GitHubbal, ami azt jelenti, hogy egyszerűen feltöltheti a kódját a GitHubra, és a Cloudflare Pages elvégzi a többit – a buildet, a telepítést és még a frissítéseket is.

Így működik:

  1. Integrált munkafolyamat: A Cloudflare Pages a git munkafolyamat köré épül. Miután csatlakoztatta GitHub repository-ját a Cloudflare Pages-hez, az minden alkalommal elkezdi építeni és telepíteni a webhelyét, amikor a kiválasztott branch-re push-ol.
  2. JAMstack-optimalizált: A Cloudflare Pages támogatja a JAMstack elveket, ami azt jelenti, hogy az Ön által preferált statikus oldalgenerátorral vagy JavaScript keretrendszerrel építheti webhelyét, beleértve, de nem kizárólagosan a Jekyll-t, Hugo-t, Next.js-t és React-ot.
  3. Gyors és biztonságos kézbesítés: A globálisan elosztott Cloudflare hálózat által hajtva a Pages biztosítja, hogy webhelye elérhető és gyors legyen, függetlenül attól, hol van a közönsége. Emellett a Cloudflare beépített biztonsági funkciói megvédik webhelyét a fenyegetésektől.
  4. Folyamatos telepítés: A Cloudflare Pages automatikusan építi és telepíti webhelyét minden alkalommal, amikor frissítéseket végez a GitHub repository-jában. Ez lehetővé teszi a gyors iterációt és a telepítési folyamatot könnyűvé teszi.
  5. Egyedi domain és HTTPS: A Pages segítségével egyedi domaint csatlakoztathat webhelyéhez, és ingyenes, automatikus HTTPS-t biztosít minden webhelyen, hogy a kapcsolat mindig biztonságos legyen.
  6. Előnézeti telepítések: Amikor új pull request-et hoz létre a csatolt GitHub repository-jában, a Cloudflare Pages automatikusan egyedi előnézeti URL-t generál, lehetővé téve a változtatások megtekintését az éles indítás előtt.

Akár egyedülálló fejlesztő, akár egy nagy csapat tagja, a Cloudflare Pages egyszerű, gyors és biztonságos módot kínál webhelyei online elérhetővé tételéhez.

A fentiek fényében ehhez a felhasználói regisztrációs rendszerhez tiszta és egyszerű HTML oldalakat választottam, mellőzve bármilyen további keretrendszert vagy build eszközt. Ez a megközelítés páratlan egyszerűséget biztosít és rugalmasságot ad bármilyen kívánt eredmény eléréséhez.

Mi az a Cloudflare Workers?

A Cloudflare Workers egy innovatív szerver nélküli számítási platform, amely lehetővé teszi a fejlesztők számára, hogy kódjukat közvetlenül a Cloudflare kiterjedt hálózatára telepítsék, amely világszerte több mint 200 várost ölel fel. Lényegében lehetővé teszi az alkalmazások számára, hogy a lehető legközelebb fussanak a végfelhasználókhoz, ezáltal csökkentve a késleltetést és javítva a felhasználói élményt.

Íme egy áttekintés a funkcióiról és előnyeiről:

  1. Szerver nélküli végrehajtási környezet: A Cloudflare Workers szerver nélküli környezetben működik, ami azt jelenti, hogy a fejlesztőknek nem kell szervereket kezelniük vagy karbantartaniuk. Ehelyett a kódírásra koncentrálhatnak, míg a platform elvégzi a többit, az elosztástól a skálázásig.
  2. Edge Computing: A hagyományos modellekkel ellentétben, ahol az alkalmazások egyetlen szerveren vagy adatközpontban futnak, a Cloudflare Workers a kódot a Cloudflare hálózat szélére viszi. Ez biztosítja, hogy alkalmazása közelebb fusson a felhasználóhoz, javított teljesítményt és sebességet nyújtva.
  3. Nyelvi rugalmasság: A Workers a V8 JavaScript motort használja, ugyanazt a futtatókörnyezetet, amelyet a Chrome is alkalmaz, lehetővé téve a fejlesztők számára, hogy JavaScript-ben írjanak kódot. Ráadásul a WebAssembly támogatásnak köszönhetően más nyelvek, mint a Rust, C és C++ is használhatók.
  4. Biztonság: A Cloudflare hálózat eredendő biztonságának kihasználásával a Workers segít megvédeni az alkalmazásokat különböző fenyegetésektől, például DDoS támadásoktól.

A Cloudflare Workers innovatív és rendkívül skálázható megoldást kínál azoknak a fejlesztőknek, akik alkalmazásaik teljesítményét, megbízhatóságát és biztonságát szeretnék növelni.

A Cloudflare Pages-en belül a Workers egy functions nevű könyvtárban találhatók. Minden JavaScript/TypeScript kódomat ebbe a térbe helyeztem, kihasználva a Workers által kínált átfogó képességeket.

Mi az a Cloudflare Workers KV?

A Cloudflare Workers KV (Key-Value) egy globálisan elosztott, eventually consistent kulcs-érték tárolórendszer, amely lehetővé teszi adatok tárolását és elérését bárhonnan a Cloudflare Workers szkriptjein belül. Úgy tervezték, hogy segítsen a szerver nélküli környezetekben az állapotkezelés skálázásában és egyszerűsítésében.

Íme a legfontosabb jellemzői és előnyei:

  1. Globális elosztás: A Cloudflare Workers KV a Cloudflare hálózatra épül, amely világszerte több mint 300 várost ölel fel. Ez biztosítja, hogy adatai a felhasználók közelében legyenek tárolva és elérhetők, csökkentve a késleltetést és javítva alkalmazásai általános teljesítményét.
  2. Gyors olvasás és írás: A Workers KV alacsony késleltetésű adathozzáférést biztosít, amely különböző alkalmazásokhoz alkalmas. Míg az írási műveletek valamivel tovább tartanak a globális propagálódásig (általában néhány másodperc), az olvasási műveletek jellemzően gyorsak, ami ideálissá teszi olvasás-intenzív munkaterhelésekhez.
  3. Nagy léptékű: Egyetlen Workers KV namespace-ben milliárdnyi kulcsot tárolhat, és minden kulcs akár 25 MB méretű értéket is tartalmazhat.
  4. Namespace-ek: A KV namespace-ek konténerek a kulcs-érték párjaihoz. Lehetővé teszik a különböző adatkészletek elkülönítését a Workers KV tárolón belül, ami különösen hasznos lehet több alkalmazás vagy környezet (mint a staging és production) kezelésekor.
  5. Eventual Consistency: A Workers KV eventual consistency-t használ. Ez azt jelenti, hogy az adatok frissítései (írások) globálisan terjednek és idővel konzisztenssé válnak, ami általában néhány másodperc kérdése.

A Cloudflare Workers KV egyedülálló megoldást kínál a szerver nélküli környezetekben az állapotkezeléshez, megbízható, gyors és globálisan elosztott adattárolási rendszert biztosítva a fejlesztőknek.

Ennek a felhasználói regisztrációs rendszernek a fejlesztése során stratégiailag a következő Workers KV namespace-eket terveztem:

  • USERS: Ez szolgál elsődleges tárolóként az összes felhasználó számára. Úgy tervezték, hogy lényegében végtelen számú rekordot kezeljen.
  • USERS_LOGIN_HISTORY: Egy dedikált tér a bejelentkezési tevékenységek rögzítésére, lehetővé téve a felhasználók számára fiókjuk biztonsági lábnyomának rendszeres értékelését.
  • USERS_SESSIONS: Ez a namespace rögzíti az aktuálisan bejelentkezett felhasználó adatait, beleértve az egyedi azonosítókat, eszközöket, helyszíneket és egyebeket.
  • USERS_SESSIONS_MAPPING: A Workers KV eventual consistency modellje miatt késések lehetnek a USERS_SESSIONS-be való írás és az ellenőrzés között. Ez különösen valószínű, ha a műveletek különböző edge helyszíneken történnek. Ennek kiküszöbölésére a validáció után közvetlenül hozzáadom az új session UID-t a USERS_SESSIONS_MAPPING-hez, biztosítva annak befoglalását még a USERS_SESSIONS-be való írás előtt.
  • USERS_TEMP: Ezt a namespace-t ideiglenes (átmeneti) linkek és egyéb, előre meghatározott lejárattal rendelkező tartalmak tárolójaként használtam.

Korlátlan kapacitású, automatikusan skálázódó és magas rendelkezésre állású adatbázisokat hoztunk létre – olyan funkciókat, amelyek gyakran csak drágább adatbázisoknál találhatók meg.

A projekt tervezése

Célom az volt, hogy valami egyszerűt és hatékonyt alkossak harmadik féltől származó könyvtárakra való támaszkodás nélkül, és ez sikerült is. Így bontakozik ki a teljes projektstruktúra:

 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

Hogy tiszta képet adjak a fenti architektúráról, nézzük meg a részletes lebontást:

  • framework: Ez a könyvtár tartalmazza az alapvető TypeScript kódunkat. Minden az adatmodellektől az e-mail sablonokig itt található, egységes megközelítést biztosítva az egész rendszerünkben.
  • functions: Itt találja a kifejezetten Cloudflare Pages Functions-höz készített TypeScript kódot, amely optimalizálja a webhely háttérműveleteit.
  • public: Minden nyilvánosan elérhető statikus HTML fájlunk ebben a mappában található, alkotva a felhasználók számára látható felületet.

Egyszerűen fogalmazva: amikor a login.html oldalra navigál, a Cloudflare Pages azonnal akcióba lép és végrehajtja a megfelelő login.ts kódot. Ez a dinamikus kölcsönhatás folytatódik az összes oldal és azok társított funkciói között.

Ezzel a beállítással zökkenőmentesen kezelhetünk számos feladatot. Legyen szó tartalomátírásról, adatfeldolgozásról vagy adatlekérésről a Cloudflare Workers KV-n keresztül – minden hatékonyan van kezelve.

A felhasználói regisztrációs rendszer felépítése

Utazásunk elindításaként először egy felhasználói regisztrációs rendszert hozunk létre. Ez az alapfunkcionalitás.

  1. Első lépésünk egy egyszerű HTML űrlap tervezése és elhelyezése a register.html-ben, amelyet a megadott adatok feldolgozásáért felelős funkció elkészítése követ:
 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>

Az űrlap beállítása után a következő lépés az adatküldés javítása JavaScript segítségével. Míg a hagyományos űrlapküldés tökéletesen működőképes, a példámban beépítettem a Bootstrap 5 validációt, így az AJAX-os adatküldés felé hajlok.

Íme egy működő példa a demómból:

  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})();
Miután a Create Account gombra kattintanak, az adatok AJAX-on keresztül a /register-re kerülnek elküldésre, ami kiváltja a functions/register.ts kódot. Ez a mechanizmus lehetővé teszi mind a GET, mind a POST adatfeldolgozást. Az OPTIONS kezelése lehetséges, de ezt a tárgyalásban mellőzzük.

  1. Standard oldallátogatásoknál, vagy GET végrehajtásoknál lehetőség van számos fejlett funkció bevezetésére, például CSRF védelem. Nézzük meg a következő példát:
 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  

Ebben a folyamatban a Cloudflare Workers HTMLRewriter funkcióját használjuk, hogy minden oldalkérésnél egyedi CSRF tokent generáljunk és ágyazzunk be az űrlapba.

A CSRF stratégiámat a következő szakaszokban részletesebben kifejtem. Egyelőre figyelje meg, hogyan fűzök dinamikusan egyedi, véletlenszerű CSRF kódot a biztonság megerősítéséhez.

  1. Most térjünk át a POST végrehajtási fázisra. Itt gondosan validáljuk a bemeneti adatokat, biztonságosan tároljuk őket, majd e-mailt küldünk a felhasználónak megerősítésre vagy további utasításokért. Hogy konceptuális áttekintést nyújtsak, elkészítettem egy pszeudo-kód ábrázolást:
 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  

A konfigurációmban a sikeres regisztráció után a felhasználó adatai a következőképpen tárolódnak a Workers KV-ben:

 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}

Ezt a formátumot igényei szerint módosíthatja, mezőket adhat hozzá vagy távolíthat el. Hangsúlyoznom kell, hogy nem fogok teljes, másolásra kész kódot biztosítani. Fontos, hogy befektessen a megértésbe és a gyakorlati felfedezésbe. Csak így fejlődhet igazán fejlesztőként.

A felhasználói bejelentkezési rendszer felépítése

A Cloudflare Pages projektjéhez tartozó bejelentkezési mechanizmusra való átállás gyerekjáték. A regisztrációs folyamathoz hasonló megközelítést fogunk alkalmazni.

  1. Ahogy a regisztrációs űrlapot elkészítettük, készítsen egy tömör űrlapot a bejelentkezési folyamathoz és helyezze el a login.html-ben:
 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. Az űrlap beállítása után ideje feldolgozni annak renderelését. A következő lépés a szükséges kód elkészítése. Ez automatikusan aktiválódik a login.ts-ből:
 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  
  1. A befejező lépés a POST végrehajtást kezelő kód megformulázása, amely a login.ts-ben található:
 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  

E 3 optimalizált lépésen keresztül zökkenőmentesen felépítettük a bejelentkezési rendszert. Kétségtelenül a bejelentkezési mechanizmus a legbonyolultabb szegmens, amely több műveletet igényel az annotáció szerint. Mégis, tekintse ezt nem bonyodalomnak, hanem lelkesítő kihívásnak, amelyet le kell küzdeni!

A jelszó-visszaállítási rendszer felépítése

  1. Hasonlóan korábbi komponenseinkhez, a jelszó-visszaállítás kezdeményezéséhez szükséges dedikált űrlap elkészítésével kezdünk:
 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>

Az alapkoncepció egyszerű: a felhasználó e-mail címének megadásakor, ha az felismerhető a rendszerünkben, egyedi jelszó-visszaállítási link generálódik és kerül kiküldésre. A fokozott biztonság érdekében gondoskodjon arról, hogy ez a link csak rövid ideig legyen aktív, ideálisan legfeljebb 2 óráig.

  1. Kezdje el a GET végrehajtást kezelő kód elkészítését, amelyet a forgot-password.ts-be kell beágyaznia:
 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  
  1. Folytassa a POST végrehajtásért felelős kód megformulázásával, biztosítva, hogy ugyanabban a forgot-password.ts fájlban legyen elhelyezve:
 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  

Most Önön a sor, hogy befejezze a kódolási fejtörőt. A megjegyzéseimben található útmutatás követésével a folyamatot elég kezelhetőnek fogja találni. Ha elkészült, egy teljesen működőképes jelszó-visszaállítási rendszerrel fog rendelkezni.

Óvatossági megjegyzés: Kerülje az olyan üzenetek megjelenítését, mint „Nem létezik ilyen e-mail cím." Az ilyen jelzők aranybányák a hackerek számára, utat nyitva a potenciális brute force támadásoknak. Ehelyett alkalmazzon kétértelműbb, de felhasználóbarát megközelítést: „Ha az e-mail regisztrálva van a rendszerünkben, visszaállítási linket fog kapni."

Felhasználói regisztrációs demó

Fedezze fel az általam létrehozott felhasználói regisztrációs folyamat élő demóját: https://members.mecanik.dev/

A funkciók közé tartozik:

Jelenleg más projektekkel is foglalkozom, de továbbra is fejlesztem ezt a platformot. Nyugodtan tesztelje – csak győződjön meg róla, hogy valódi e-mail címeket használ, hogy ne befolyásolja a SendGrid reputációmat.

Minden visszajelzést és hibajelentést szívesen fogadok. Kérem, keressen meg, ha bármije van!

Kérdések és válaszok

Kétségtelenül a biztonság a legfontosabb szempont. Kérdőjelezheti a rendszer robusztusságát a potenciális támadásokkal és adatszivárgásokkal szemben.

Vegye figyelembe, hogy a Cloudflare Pages a Cloudflare platformon működik, hozzáférést biztosítva a fejlett Web Application Firewall-jukhoz (WAF) és biztonsági eszközeik sorához.

Hogyan működik valójában a CSRF?

Talán megfigyelte a CSRF token dinamikus generálását és űrlapba való injektálását minden betöltéskor. Így biztosítja ez a mechanizmus a kérések biztonságát:

A CSRF token komponensei:

  • IP: Rögzíti a felhasználó aktuális IP-címét.
  • Country: Azonosítja a felhasználó aktuális tartózkodási helyét.
  • UserAgent: Rögzíti a felhasználó böngészőjének adatait.
  • expiry: Időzítőt állít be egy perc hozzáadásával az aktuális időhöz.

Ezek az adatok JSON formátumban kerülnek összeállításra: {i, c, u, e}, amelyet aztán titkosítanak és Hex formátumba alakítanak:

1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));

A titkosítási funkcióról:

  1. Az adatok titkosítása felhasználó által definiált jelszóval történik, jelszókulcs generálásával, majd abból AES kulcs levezetésével.
  2. Ez az AES kulcs titkosítja az adatokat, a salt és IV (inicializálási vektor) menet közben generálódik.
  3. A titkosított kimenet egy ArrayBuffer, amely tartalmazza az iterációk számát, a salt-ot, az IV-t és a titkosított adatokat.

Egyszerűen fogalmazva, ez a titkosítási folyamat iparági szabványos gyakorlatokat követ, hogy a titkosított adatok megfejthetelenek és manipuláció ellen védettek legyenek.

A CSRF token validálása:

 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}

A validálási folyamat szigorúan ellenőrzi a CSRF tokent, biztosítva, hogy:

  • Ugyanarról az IP-címről származik.
  • Ugyanabból az országból lett elküldve.
  • Ugyanabból a böngészőből lett elküldve.
  • Az aktív időkeretén belül lett benyújtva.
  • Ha ezen ellenőrzések bármelyike meghiúsul, a rendszer érvénytelennek azonosítja a CSRF tokent, robusztus védelmet nyújtva a potenciális fenyegetések ellen.

Ha ezen ellenőrzések bármelyike meghiúsul, a rendszer érvénytelennek azonosítja a CSRF tokent, robusztus védelmet nyújtva a potenciális fenyegetések ellen.

Jelszó hash-elés/titkosítás

A jelszó hash-eléshez/titkosításhoz a PBKDF2 (Password-Based Key Derivation Function 2) algoritmust használtam a SHA-256 hash algoritmussal együtt.

A módszer minden jelszóhoz egyedi, kriptográfiailag biztonságos pszeudo-véletlenszerű „salt"-ot használ, biztosítva a fokozott biztonságot. Ez a megközelítés védelmet nyújt a rainbow table és brute force támadások ellen.

A jelszó hash-elt verziója kerül tárolásra, nem a nyílt szöveg, tovább erősítve az adatintegritást.

Miért nincs JWT token használat?

A JWT (JSON Web Tokens) népszerű módszer a felhasználói hitelesítés kezelésére és információk továbbítására a felek között kompakt, URL-biztos formátumban.

Bár számos előnyük van, vannak okok, amiért bizonyos kontextusokban, például egy teljes felhasználói regisztrációs rendszerben, érdemes lehet nem használni őket. Íme egy mélyebb áttekintés:

  1. Állapotmentesség és visszavonás: A JWT-k egyik jellemzője az állapotmentesség. Ez a tulajdonság azonban problémás lehet felhasználókezelő rendszerek számára. Például ha egy felhasználó JWT tokenje ellopják vagy kompromittálják, nincs egyszerű módja a token visszavonásának, hacsak nem tart fenn egy tiltólistát a tokenekről, ami ellentmond az állapotmentesség céljának.
  2. Méret: Minél több adatot ad hozzá egy JWT-hez, annál nagyobb lesz. Ha átfogó felhasználói regisztrációs rendszerrel rendelkezik, ahol esetleg több felhasználóhoz kapcsolódó adatot kell a tokenben tárolni, ez nagyobb HTTP headerekhez és megnövekedett késleltetéshez vezethet.
  3. Tárolási biztonság: A JWT-k kliensoldali tárolásához a gyakori tárolóhelyek közé tartozik a local storage vagy a cookie-k. A local storage sebezhető az XSS támadásokkal szemben, és bár a cookie-k nagyobb mértékben biztosíthatók, CSRF támadások lehetőségét nyitják meg, ha nem megfelelően kezelik őket.
  4. Lejárat kezelése: A JWT-k lejáratának kezelése összetett lehet. Míg a rövid élettartamú tokenek csökkentik a kockázatot, refresh mechanizmust igényelnek, ami több bonyolultságot visz a hitelesítési folyamatba.
  5. Nincs beépített visszavonás: Mint korábban említettük, ha egy token kompromittálódik, nincs inherens mechanizmus annak visszavonására vagy érvénytelenítésére, amíg le nem jár. Ez jelentős biztonsági kockázatot jelenthet, ha a token hosszú élettartammal rendelkezik.
  6. Komplexitás a fejlesztők számára: Azok számára, akik nem ismerik a JWT-ket, tanulási görbe társul annak megértéséhez, hogyan kell őket megfelelően generálni, validálni és használni. Ez a komplexitás hibákat és sebezhetőségeket okozhat, ha nem értik és implementálják alaposan.

Különböző rendszerek és a felhasználói adataik kezelésének vizsgálatánál azt tapasztaltam, hogy egyes vállalatok rengeteg információt tárolnak JWT tokenjeikben, beleértve az érzékeny adatokat, mint a felhasználói szerepek és mások.

Tisztán szeretném leszögezni: szándékom nem más megközelítések kritizálása vagy befeketítése. Azonban a biztonságról alkotott szakmai álláspontom alapján az ilyen kritikus információk közvetlen tokenekbe való beágyazása aggályokat vet fel. Inherens kockázat társul a szükségesnél több adat kitéséhez, különösen amikor ezek az adatok potenciálisan kihasználhatók felhasználói jogosultságok vagy egyéb funkcionalitások kiderítésére.

Ezen fenntartások alapján tudatos döntést hoztam, hogy eltérjek ettől a módszertől. Ehelyett a bevált szerveroldali hitelesítés felé fordultam, biztonságos cookie kezeléssel kombinálva. Ez a megközelítés véleményem szerint egyensúlyt teremt a felhasználói élmény és a robusztus biztonság között, biztosítva, hogy az érzékeny adatok védettek maradjanak és a rendszer ellenálló legyen a potenciális sebezhetőségekkel szemben.

E-mailek megbízható küldése

A SendGrid-et választottam dinamikus sablonokkal, széleskörű elterjedtsége és az árnyalataival való mély ismeretségem miatt. A mélyebb megismeréshez tekintse meg az API dokumentációt .

Bár az integrációja okozhat némi bonyodalmat, ez mind része a jutalmazó kihívásnak. Íme egy példa Önnek:

 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});

Hogyan lehet tovább növelni a biztonságot?

Bár a Cloudflare Workers KV titkosítja a tárolt adatokat, további titkosítási réteget adhat a felhasználói adatokhoz.

Vagy szeretne csavart adni a potenciális támadóknak? Fontolja meg dummy űrlapbemenetek implementálását a botok és hackerek összezavarására:

 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);

Ez a kódrészlet dinamikusan generál 1 és 5 közötti véletlenszerűen elnevezett beviteli mezőt minden oldalbetöltésnél.

A lehetőségek szinte korlátlanok, csak a képzeletét kell használnia.

Összefoglalás

És itt is vagyunk! Őszintén remélem, hogy ez az írás tanulságos és lebilincselő volt az Ön számára. Akár tapasztalt fejlesztő, akár most merül el a skálázható felhőalapú weboldalak világában, a tudás megosztása létfontosságú közösségünk növekedéséhez.

Ha értékesnek találta ezt a cikket, kérjük, fontolja meg annak továbbítását fejlesztőtársainak vagy a téma iránt érdeklődőknek. Minden megosztás szélesíti kollektív tudásunkat.

Visszajelzéseit és kérdéseit rendkívül nagyra értékeljük. Ne habozzon kommentet írni vagy felvenni a kapcsolatot – tartsuk fenn a párbeszédet. Biztonságos kódolást!