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:
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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:
- 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.
- 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.
- 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.
- 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.
- 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 aUSERS_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.htmlHogy 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.
- 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})();/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.- 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.
- 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.
- 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>- 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
- 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
- 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.
- 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
- Folytassa a POST végrehajtásért felelős kód megformulázásával, biztosítva, hogy ugyanabban a
forgot-password.tsfá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:
- Regisztráció, bejelentkezés és jelszó-visszaállítás
- Hozzáférés a profiljához
- Biztonsági beállítások és aktív munkamenetek kezelése – Leválaszthatja magát például, ha a mobiltelefonjáról jelentkezett be.
- Ingyenes szoftverek előnézete és közelgő prémium ajánlatok figyelése.
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:
- 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.
- Ez az AES kulcs titkosítja az adatokat, a salt és IV (inicializálási vektor) menet közben generálódik.
- 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:
- Á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.
- 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.
- 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.
- 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.
- 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.
- 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!
Hozzászólások