În peisajul în continuă evoluție al dezvoltării web, aplicațiile serverless își demonstrează constant prezența. Avantajele lor incontestabile, cum ar fi scalabilitatea fără egal și performanța robustă, le fac să iasă în evidență. Frumusețea serverless-ului constă în promisiunea sa: de a furniza soluții puternice care nu doar se scalează fără efort, ci sunt și eficiente din punct de vedere al costurilor.
Ați visat vreodată să lansați o idee de afaceri, dar v-ați împiedicat de complexitățile tehnice ale construirii unui sistem de înregistrare și autentificare a utilizatorilor? Deși platforme precum Netlify și diverse baze de date bazate pe cloud există, ele nu se compară cu ceea ce oferă Cloudflare. În timp ce multe alternative pot vedea o creștere exponențială a costurilor odată cu scalarea, situația este diferită cu Cloudflare Pages.
Am lucrat intermitent la acest proiect și demo pe parcursul mai multor luni, alternând cu alte sarcini. Îmi cer scuze pentru așteptare, mai ales celor care au așteptat cu nerăbdare acest sistem .
Eliberați puterea Cloudflare Pages
- Scalabilitate fără efort: Gestionați înregistrări nelimitate de utilizatori fără probleme.
- Eficiență a costurilor: Spuneți adio cheltuielilor neașteptate; bucurați-vă de prețuri consistente.
- Performanță ultrarapidă: Experimentați performanțe cum nu ați mai experimentat până acum.
- Securitate solidă: Datele utilizatorilor dumneavoastră rămân protejate și securizate.
Tehnologiile pe care le vom folosi
- Cloudflare Pages
- Cloudflare Pages Functions
- Cloudflare Workers KV
Ce este Cloudflare Pages?
Cloudflare Pages este o platformă modernă și accesibilă, concepută pentru dezvoltatori, pentru a construi, implementa și găzdui site-uri web. Oferă o integrare perfectă cu GitHub, ceea ce înseamnă că tot ce trebuie să faceți este să trimiteți codul pe GitHub, iar Cloudflare Pages se ocupă de restul – compilare, implementare și chiar actualizări.
Iată cum funcționează:
- Flux de lucru integrat: Cloudflare Pages este construit în jurul fluxului de lucru git. Odată ce conectați un repository GitHub la Cloudflare Pages, acesta va compila și implementa automat site-ul dumneavoastră de fiecare dată când faceți push pe branch-ul selectat.
- Optimizat pentru JAMstack: Cloudflare Pages suportă principiile JAMstack, ceea ce înseamnă că puteți construi site-ul cu generatorul static preferat sau framework-ul JavaScript la alegere, inclusiv Jekyll, Hugo, Next.js, React și altele.
- Livrare rapidă și securizată: Alimentat de rețeaua distribuită global a Cloudflare, Pages asigură că site-ul dumneavoastră este disponibil și rapid de accesat, indiferent unde se află audiența. În plus, funcțiile de securitate proprietare ale Cloudflare protejează site-ul de amenințări.
- Implementare continuă: Cloudflare Pages compilează și implementează automat site-ul de fiecare dată când faceți o actualizare în repository-ul GitHub. Aceasta permite iterații rapide și simplifică enorm procesul de implementare.
- Domenii personalizate și HTTPS: Cu Pages, puteți atașa domenii personalizate site-ului, iar HTTPS automat gratuit este furnizat pentru fiecare site, garantând că conexiunile sunt întotdeauna securizate.
- Implementări de previzualizare: De fiecare dată când creați un nou pull request pe repository-ul GitHub conectat, Cloudflare Pages generează automat un URL unic de previzualizare pentru a verifica modificările înainte de a le pune live.
Fie că sunteți un dezvoltator individual sau parte dintr-o echipă mare, Cloudflare Pages oferă o modalitate ușoară, rapidă și securizată de a vă publica site-ul online.
Având în vedere cele de mai sus, pentru acest sistem de înregistrare a utilizatorilor, am ales pagini HTML simple, fără framework-uri sau instrumente de compilare suplimentare. Această abordare garantează o simplitate fără egal și oferă flexibilitatea de a obține rezultatele dorite.
Ce sunt Cloudflare Workers?
Cloudflare Workers este o platformă revoluționară de calcul serverless care permite dezvoltatorilor să implementeze codul direct în rețeaua vastă a Cloudflare, care se întinde în peste 200 de orașe din întreaga lume. În esență, se asigură că aplicațiile dumneavoastră rulează cât mai aproape de utilizatorul final, reducând latența și îmbunătățind experiența utilizatorului.
Iată o prezentare a funcțiilor și beneficiilor sale:
- Mediu de execuție serverless: Cloudflare Workers funcționează într-un mediu serverless. Aceasta înseamnă că dezvoltatorii nu trebuie să gestioneze sau să întrețină servere. În schimb, se pot concentra pe scrierea codului, iar platforma se ocupă de restul, de la implementare la scalare.
- Calcul la margine (Edge Computing): Spre deosebire de modelele tradiționale în care aplicațiile rulează pe un singur server sau centru de date, Cloudflare Workers aduce codul la marginea rețelei Cloudflare. Aceasta asigură că aplicațiile rulează mai aproape de utilizatori, oferind performanță și viteză îmbunătățite.
- Flexibilitate lingvistică: Workers utilizează motorul V8 JavaScript – același runtime ca Chrome – permițând dezvoltatorilor să scrie cod în JavaScript. Mai mult, datorită suportului WebAssembly, pot fi folosite și alte limbaje precum Rust, C și C++.
- Securitate: Valorificând securitatea inerentă a rețelei Cloudflare, Workers protejează aplicațiile de diverse amenințări, inclusiv atacuri DDoS.
Cloudflare Workers oferă o soluție inovatoare și foarte scalabilă pentru dezvoltatorii care doresc să îmbunătățească performanța, fiabilitatea și securitatea aplicațiilor lor.
În cadrul Cloudflare Pages, Workers sunt plasați într-un director numit functions. Am plasat tot codul JavaScript/TypeScript în acest spațiu, profitând de capacitățile cuprinzătoare pe care Workers le oferă.
Ce este Cloudflare Workers KV?
Cloudflare Workers KV (Key-Value) este un depozit de date distribuit global cu consistență eventuală, care vă permite să stocați și să accesați date din cadrul scripturilor Cloudflare Workers de oriunde din lume. Este conceput pentru a ajuta la scalarea și simplificarea gestionării stării în mediul serverless.
Iată caracteristicile și beneficiile principale:
- Distribuție globală: Cloudflare Workers KV este construit pe rețeaua Cloudflare, care se întinde în peste 300 de orașe din întreaga lume. Aceasta asigură că datele sunt stocate și accesate aproape de utilizatori, reducând latența și îmbunătățind performanța generală a aplicațiilor.
- Citiri și scrieri rapide: Workers KV furnizează acces la date cu latență redusă, potrivit pentru o varietate de aplicații. În timp ce scrierile pot dura puțin timp pentru a se propaga global (de obicei câteva secunde), operațiunile de citire sunt în general rapide, făcându-l ideal pentru sarcini intensive de citire.
- Scară mare: Puteți stoca miliarde de chei într-un singur namespace Workers KV, iar fiecare cheie poate deține o valoare de până la 25MB.
- Namespace-uri: Namespace-urile KV sunt containere pentru perechi cheie-valoare. Vă permit să separați diferite seturi de date în cadrul depozitului Workers KV, util în special pentru gestionarea mai multor aplicații sau medii (cum ar fi staging și producție).
- Consistență eventuală: Workers KV utilizează consistența eventuală, ceea ce înseamnă că actualizările de date (scrierile) se propagă global, asigurând consistența în timp – de obicei o chestiune de secunde.
Cloudflare Workers KV prezintă o soluție unică pentru gestionarea stării în mediul serverless, oferind dezvoltatorilor un sistem de stocare a datelor fiabil, rapid și distribuit global.
Pe parcursul dezvoltării acestui sistem de înregistrare a utilizatorilor, am conceput strategic următoarele namespace-uri Workers KV:
- USERS: Servește ca depozit principal pentru toți utilizatorii. Este proiectat să gestioneze un volum practic nelimitat de înregistrări.
- USERS_LOGIN_HISTORY: Un spațiu dedicat pentru jurnalizarea activității de autentificare, permițând utilizatorilor să evalueze periodic starea de securitate a contului lor.
- USERS_SESSIONS: Acest namespace captează detaliile utilizatorilor conectați în prezent. Include ID-uri unice, dispozitive, locații și altele.
- USERS_SESSIONS_MAPPING: Dat fiind modelul de consistență eventuală al Workers KV, pot apărea întârzieri între scrierea în
USERS_SESSIONSși verificarea ulterioară, în special dacă operațiunile au loc la locații edge diferite. Pentru a evita acest lucru, după validare, UID-ul noii sesiuni este adăugat direct în USERS_SESSIONS_MAPPING, asigurând includerea chiar înainte de scrierea înUSERS_SESSIONS. - USERS_TEMP: Am folosit acest namespace ca depozitul pentru linkuri temporare și alt conținut cu perioade de expirare predeterminate.
Am creat o bază de date cu capacitate nelimitată, scalare automată și disponibilitate ridicată. Acestea sunt caracteristici care se regăsesc de obicei în bazele de date mai costisitoare.
Designul proiectului
Obiectivul a fost să creez ceva simplu și eficient, fără a depinde de biblioteci terțe, și am reușit. Iată structura completă a proiectului:
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.htmlPentru a înțelege clar arhitectura de mai sus, iată o analiză detaliată:
- framework: Acest director conține codul TypeScript fundamental. De la modele de date până la șabloane de email, totul este situat aici, asigurând o abordare consecventă în întregul sistem.
- functions: Aici veți găsi codul TypeScript specific Cloudflare Pages Functions, optimizând operațiunile backend ale site-ului.
- public: Toate fișierele HTML statice accesibile public sunt situate în acest folder, formând interfața vizibilă utilizatorului.
Pe scurt, atunci când navigați la pagina login.html, Cloudflare Pages intră în acțiune, executând codul corespunzător login.ts. Această interacțiune dinamică continuă pe toate paginile și funcțiile lor asociate.
Cu această configurare, diverse operațiuni sunt gestionate fără probleme. Fie că este vorba de rescriere de conținut, procesare de date sau extragere de date prin Cloudflare Workers KV, totul este gestionat eficient.
Construirea sistemului de înregistrare a utilizatorilor
Începând călătoria, vom construi mai întâi sistemul de înregistrare a utilizatorilor. Aceasta este funcționalitatea de bază.
- Primul pas este să proiectăm un formular HTML simplu, să-l plasăm în
register.htmlși să scriem o funcție pentru a procesa datele furnizate:
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>Odată ce formularul este configurat, următorul pas este să îmbunătățim trimiterea datelor folosind JavaScript. Deși trimiterea tradițională a formularului funcționează perfect, am optat pentru transmiterea datelor prin AJAX, deoarece am integrat validarea Bootstrap 5 în exemplul nostru.
Iată un exemplu funcțional din demo:
1(function () {
2'use strict'
3
4 // Check if passwords match
5 document.querySelectorAll("#password, #confirm_password").forEach(function (input) {
6 input.addEventListener("keyup", function () {
7 const password = document.getElementById("password");
8 const confirmPassword = document.getElementById("confirm_password");
9 if (
10 password.value !== "" &&
11 confirmPassword.value !== "" &&
12 confirmPassword.value === password.value
13 ) {
14 // Do something when passwords match
15 }
16 });
17 });
18
19 var form = document.querySelector("#register");
20
21 form.addEventListener('submit', function (event)
22 {
23 const _alert = document.querySelector("div.alert.alert-danger");
24
25 if(_alert)
26 _alert.remove();
27
28 if (!form.checkValidity())
29 {
30 // Prevent default form submission
31 event.preventDefault();
32 event.stopPropagation();
33 form.classList.add('was-validated');
34 }
35 else
36 {
37 // Mark inputs as validated
38 form.classList.add('was-validated');
39
40 // Prevent default form submission again
41 event.preventDefault();
42 event.stopPropagation();
43
44 // Helper to get all inputs we want
45 const getAllFormElements = element => Array.from(element.elements).filter(tag => ["input"].includes(tag.tagName.toLowerCase()));
46
47 // Grab desired inputs
48 const registerFormElements = getAllFormElements(event.currentTarget);
49
50 // Loop over them and disable
51 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
52 element.setAttribute("readonly", true);
53 element.style = 'background-color: #e9ecef; opacity: 1;';
54 });
55
56 // Disable button and show loading spinner
57 let _button = event.currentTarget.querySelector(".btn");
58 _button.setAttribute("disabled", true);
59 _button.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Please wait...';
60
61 // A modern replacement for XMLHttpRequest.
62 // https://caniuse.com/fetch
63 if (window.fetch)
64 {
65 (async() => {
66 await fetch(window.location.href, {
67 method: 'POST',
68 // NOTE: FormData will only grab inputs with name attribute
69 body: JSON.stringify(Object.fromEntries(new FormData(event.target))),
70 headers: {
71 'Accept': 'application/json',
72 'Content-Type': 'application/json'
73 }
74 }).then((response) => {
75 // Pass both the parsed JSON and the status code to the next .then()
76 return response.json().then((data) => ({status: response.status, body: data}));
77 }).then(({status, body}) => {
78
79 if (body.success === true && status === 201) {
80 Array.prototype.slice.call(document.querySelectorAll('form *')).forEach(function (element) {
81 element.style.display = "none";
82 });
83
84 let _alert = document.createElement("div");
85 _alert.classList.add('alert');
86 _alert.classList.add('alert-success');
87 _alert.setAttribute("role", 'alert');
88 _alert.innerHTML = '<p>Thank you for joining! A confirmation email has been sent out. You need to verify your email before trying to log in.</p>';
89
90 form.prepend(_alert);
91 } else {
92 let _alert = document.createElement("div");
93 _alert.classList.add('alert');
94 _alert.classList.add('alert-danger');
95 _alert.setAttribute("role", 'alert');
96 _alert.innerText = `Error #${data.error.code}: ${data.error.message}`;
97
98 form.prepend(_alert);
99
100 form.classList.remove('was-validated');
101
102 Array.prototype.slice.call(registerFormElements).forEach(function (element) {
103 element.removeAttribute("style");
104 element.removeAttribute("readonly");
105 });
106
107 _button.removeAttribute("disabled");
108 _button.innerHTML = 'Create Account';
109 }
110 });
111 })();
112 }
113 else
114 {
115 alert("Your browser is too old!\nIt does not have the most recent features.\nPlease update your browser or use a different one.");
116 }
117 }
118 }, false);
119
120})();/register, declanșând codul functions/register.ts. Acest mecanism permite procesarea atât a datelor GET, cât și POST. Gestionarea OPTIONS este posibilă, dar nu va fi inclusă în această discuție.- Pentru o vizită standard a paginii sau execuție GET, putem introduce diverse funcționalități avansate, cum ar fi protecția CSRF. Ia în considerare următorul exemplu:
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
În acest proces, funcția HTMLRewriter de la Cloudflare Workers este folosită pentru a genera un token CSRF unic pentru fiecare cerere de pagină și pentru a-l injecta în formular.
Strategia CSRF va fi detaliată în secțiunile ulterioare. Deocamdată, observați cum un cod CSRF unic aleatoriu este adăugat dinamic pentru a consolida securitatea.
- Acum trecem la faza de execuție POST, unde datele de intrare sunt validate cu atenție, stocate în siguranță și apoi se trimite un email utilizatorului pentru confirmare sau instrucțiuni suplimentare. Am creat o reprezentare în pseudo-cod pentru a oferi o imagine de ansamblu conceptuală:
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
În configurarea mea, după o înregistrare reușită, detaliile utilizatorului sunt stocate în Workers KV după cum urmează:
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}Puteți ajusta acest format, adăugând sau eliminând câmpuri după necesitate. Trebuie subliniat că nu voi furniza cod complet, gata de copiat. Investirea în înțelegere și explorare practică este esențială. Doar astfel puteți crește cu adevărat ca dezvoltator.
Construirea sistemului de autentificare
Tranziția la mecanismul de autentificare al proiectului Cloudflare Pages este foarte simplă. Vom adopta o abordare similară procesului de înregistrare.
- La fel cum am creat formularul de înregistrare, vom crea un formular concis pentru procesul de autentificare, plasându-l în
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>- După configurarea formularului, este timpul să gestionăm randarea. Următorul pas este să scriem codul necesar. Acesta este declanșat automat din
login.ts:
1/\*\*
2
3- GET /login
4 \*/
5 export const onRequestGet: PagesFunction = async ({ next }) =>
6 {
7 // Fetch the original page content
8 // Prepare our CSRF data
9 // Rewrite the content and stream it back to the user (async)
10 };
11
- Pasul final este să scriem codul care gestionează execuția POST, situat în
login.ts:
1/\*\*
2
3- POST /login
4 \*/
5 export const onRequestPost: PagesFunction<{
6 USERS: KVNamespace;
7 USERS_SESSIONS: KVNamespace;
8 USERS_SESSIONS_MAPPING: KVNamespace;
9 USERS_LOGIN_HISTORY: KVNamespace; }> = async ({ request, env }) =>
10 {
11 // Validate content type
12 // Validate our CSRF before doing anything
13 // Check for existing user email
14 // Generate a new salt & hash the original password
15 // Compare the passwords
16 // Save session
17 // Update history
18 // Retrieve the current mapping for this user (if it exists)
19 // Add the new session UID to the mapping
20 // Store the updated mapping
21 // Etc
22 };
23
Cu acești 3 pași simplificați, am construit cu succes sistemul de autentificare. Fără îndoială, mecanismul de autentificare este cea mai complexă parte, necesitând mai multe operațiuni, așa cum se menționează în comentarii. Totuși, priviți acest lucru nu ca pe o complicație, ci ca pe o provocare interesantă de depășit!
Construirea sistemului de resetare a parolei
- Similar componentelor anterioare, începem prin crearea unui formular dedicat pentru inițierea resetării parolei:
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>Conceptul de bază este simplu: odată ce emailul utilizatorului este introdus și recunoscut în sistem, se generează un link unic de resetare a parolei, valabil o singură dată, și se trimite. Pentru o securitate sporită, asigurați-vă că acest link este activ doar pentru o perioadă scurtă – ideal nu mai mult de 2 ore.
- Începem prin scrierea codului pentru gestionarea execuției GET, integrat în
forgot-password.ts:
1/\*\*
2
3- GET /forgot-password
4 \*/
5 export const onRequestGet: PagesFunction = async ({ next }) =>
6 {
7 // Fetch the original page content
8 // Prepare our CSRF data
9 // Rewrite the content and stream it back to the user (async)
10 };
11
- Scriem codul responsabil de execuția POST, integrat în același fișier
forgot-password.ts:
1/\*\*
2
3- POST /forgot-password
4 \*/
5 export const onRequestPost: PagesFunction<{
6 USERS: KVNamespace;
7 USERS_TEMP: KVNamespace;
8 }> = async ({ request, env }) =>
9 {
10 // Validate content type
11 // Validate our CSRF before doing anything
12 // Check for existing user email
13 // Generate a unique string to hash
14 // Hash the string
15 // Save to KV, expiration 2 hours
16 // Send email to user
17 // Etc
18 };
19
Acum depinde de dumneavoastră să completați puzzle-ul de codare. Urmând indicațiile din comentarii, procesul va fi destul de ușor de gestionat. Odată finalizat, veți avea un sistem de resetare a parolei complet funcțional.
Atenție: Nu afișați mesaje de tip „Acel email nu există". Astfel de informații sunt o mină de aur pentru hackeri, deschizând calea unui potențial atac de tip brute force. În schimb, adoptați o abordare mai ambiguă, dar prietenoasă: „Dacă emailul este înregistrat în sistemul nostru, veți primi un link de resetare."
Demo înregistrare utilizatori
Explorați un demo live al procesului de înregistrare a utilizatorilor pe care l-am construit: https://members.mecanik.dev/
Funcționalitățile includ:
- Înregistrare, autentificare și resetare a parolei
- Acces la profil
- Control securitate și sesiuni active - de exemplu, dacă v-ați autentificat de pe mobil, puteți deconecta acea sesiune.
- Previzualizare software gratuit și produse premium viitoare.
Lucrez în prezent și la alte proiecte, dar voi continua să îmbunătățesc această platformă. Nu ezitați să testați - vă rog să folosiți emailuri reale pentru a nu afecta reputația mea SendGrid.
Feedback-ul și raportarea problemelor sunt binevenite. Dacă aveți ceva, nu ezitați să mă contactați!
Întrebări și răspunsuri
Fără îndoială, securitatea este primordială. S-ar putea să vă întrebați cât de robustă este sistemul împotriva atacurilor potențiale și a breșelor de date.
Luați în considerare faptul că Cloudflare Pages funcționează pe platforma Cloudflare, oferind acces la Web Application Firewall (WAF) avansat și la o suită de instrumente de securitate.
Cum funcționează CSRF de fapt?
Este posibil să fi observat că la fiecare încărcare, un token CSRF este generat dinamic și injectat în formular. Iată cum acest mecanism asigură securitatea cererilor:
Componentele tokenului CSRF:
- IP: Captează adresa IP curentă a utilizatorului.
- Country: Identifică locația curentă a utilizatorului.
- UserAgent: Înregistrează detaliile browserului utilizatorului.
- expiry: Setează un cronometru adăugând 1 minut la ora curentă.
Aceste date sunt combinate în format JSON: {i, c, u, e}. Sunt apoi criptate și convertite în Hex:
1const encryptedData = await encryptData(new TextEncoder().encode(CSRF_TOKEN), env.CSRF_SECRET, 10000);
2const hex = Utils.BytesToHex(new Uint8Array(encryptedData));Despre funcția de criptare:
- Criptează datele folosind o parolă personalizată, generând o cheie de parolă și apoi derivând o cheie AES.
- Această cheie AES criptează datele, cu salt și IV (Vector de Inițializare) generate pe loc.
- Rezultatul criptat este un ArrayBuffer care încapsulează numărul de iterații, salt, IV și datele criptate.
Pe scurt, acest proces de criptare menține practicile standard din industrie, asigurând că datele criptate sunt indescifrabile și protejate împotriva falsificării.
Validarea tokenului CSRF:
1const unhex = Utils.HexToBytes(formData.csrf);
2const decrypted = new TextDecoder().decode(await decryptData(unhex, env.CSRF_SECRET));
3const parsed = JSON.parse(decrypted);
4
5if (!Utils.isCRSFData(parsed))
6{
7return new Response(JSON.stringify({
8result: null,
9success: false,
10// An ambigous message; don't tell the hacker what's missing.
11error: { code: 1001, message: "Invalid CSRF Token. Please refresh the page and try again." }
12}),
13{
14status: 403,
15headers: { 'content-type': 'application/json;charset=UTF-8'
16}
17});
18}
19
20const IP = request.headers.get('CF-Connecting-IP');
21const Country = request.headers.get('CF-IPCountry') || '';
22const UserAgent = request.headers.get('User-Agent');
23
24if(IP !== parsed.i || Country !== parsed.c || UserAgent !== parsed.u || Date.now() > parsed.e)
25{
26return new Response(JSON.stringify({
27result: null,
28success: false,
29// An ambigous message; don't tell the hacker what's missing.
30error: { code: 1002, message: "Invalid CSRF Token. Please refresh the page and try again." }
31}),
32{
33status: 403,
34headers: { 'content-type': 'application/json;charset=UTF-8'
35}
36});
37}Procesul de validare verifică riguros tokenul CSRF, asigurând:
- Provine de la aceeași adresă IP.
- Este trimis din aceeași țară.
- Este trimis din același browser.
- Este trimis în intervalul de timp activ.
- Dacă oricare dintre aceste verificări eșuează, sistemul identifică tokenul CSRF ca invalid, oferind o protecție robustă împotriva amenințărilor potențiale.
Dacă oricare dintre aceste verificări eșuează, sistemul identifică tokenul CSRF ca invalid, oferind o protecție robustă împotriva amenințărilor potențiale.
Hashing/Criptare parole
Pentru hashing-ul/criptarea parolelor, am folosit PBKDF2 (Password-Based Key Derivation Function 2) cu algoritmul de hashing SHA-256 .
Această metodă utilizează un „salt" pseudo-aleatoriu unic, securizat criptografic, pentru fiecare parolă, asigurând o securitate consolidată. Această abordare oferă protecție împotriva atacurilor de tip rainbow table și brute force.
Un hash al parolei este stocat, nu textul în clar, consolidând și mai mult integritatea datelor.
De ce nu JWT Tokens?
JWT (JSON Web Tokens) este o metodă populară de gestionare a autentificării utilizatorilor și de transfer al informațiilor între părți într-un mod compact și sigur pentru URL-uri.
Deși au multe avantaje, există motive pentru a nu le folosi în anumite situații, cum ar fi un sistem complet de înregistrare a utilizatorilor. Să explorăm:
- Fără stare și revocare: Una dintre caracteristicile JWT este natura sa fără stare. Cu toate acestea, această proprietate poate fi problematică în sistemele de gestionare a utilizatorilor. De exemplu, dacă tokenul JWT al unui utilizator este furat sau compromis, nu există o modalitate simplă de a-l revoca fără a menține o listă neagră de tokenuri, ceea ce anulează scopul naturii fără stare.
- Dimensiune: Pe măsură ce adăugați mai multe date într-un JWT, dimensiunea acestuia crește. Dacă aveți un sistem cuprinzător de înregistrare a utilizatorilor care necesită stocarea mai multor date legate de utilizator în token, acest lucru poate duce la headere HTTP mai mari și latență crescută.
- Securitatea stocării: Pentru stocarea pe partea clientului a JWT-urilor, locațiile comune includ local storage sau cookie-uri. Local storage este vulnerabil la atacuri XSS, iar cookie-urile, deși pot fi mai protejate, pot deschide calea atacurilor CSRF dacă nu sunt gestionate corect.
- Gestionarea expirării: Gestionarea expirării JWT poate fi complexă. Tokenurile pe termen scurt reduc riscurile, dar necesită mecanisme de reîmprospătare, introducând mai multă complexitate în fluxul de autentificare.
- Fără revocare încorporată: Așa cum am menționat anterior, dacă un token este compromis, nu există un mecanism inerent de a-l revoca sau invalida până la expirare. Aceasta poate fi un risc semnificativ de securitate dacă tokenul are o durată de viață lungă.
- Complexitate pentru dezvoltatori: Pentru cei nefamiliarizați cu JWT-urile, există o curbă de învățare în înțelegerea modului de creare, validare și utilizare corectă. Această complexitate poate duce la greșeli și vulnerabilități dacă nu este înțeleasă și implementată temeinic.
Explorând diverse sisteme și modurile lor de gestionare a datelor de utilizator, am descoperit că unele companii stochează o cantitate enormă de informații, inclusiv date sensibile precum rolurile utilizatorilor, în tokenurile JWT.
Pentru claritate, nu este intenția mea să critic sau să denigrez alte abordări. Cu toate acestea, din perspectiva mea profesională privind securitatea, includerea unor astfel de informații critice direct într-un token ridică îngrijorări. Există un risc inerent în expunerea mai multor date decât este necesar, mai ales atunci când acele date pot fi exploatate pentru a deduce permisiunile utilizatorilor sau alte funcționalități.
Având în vedere aceste preocupări, am luat decizia conștientă de a mă abate de la această metodă. În schimb, m-am orientat către o combinație de autentificare verificată pe partea serverului și gestionare securizată a cookie-urilor. Această abordare, în opinia mea, creează un echilibru între experiența utilizatorului și securitatea robustă, asigurând că datele sensibile sunt protejate și că sistemul rămâne rezistent la vulnerabilități potențiale.
Trimiterea fiabilă a emailurilor
Pe baza adoptării sale pe scară largă și a înțelegerii profunde a nuanțelor sale, am ales SendGrid cu șabloane dinamice. Pentru mai multe informații, consultați documentația API .
Deși integrarea poate fi complexă, totul face parte din provocarea plină de satisfacții. Iată un exemplu:
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});Cum pot îmbunătăți și mai mult securitatea?
Chiar dacă Cloudflare Workers KV criptează datele stocate, puteți adăuga un strat suplimentar de criptare datelor utilizatorilor.
Alternativ, doriți să le dați de lucru atacatorilor potențiali? Luați în considerare implementarea de câmpuri dummy în formulare care să deruteze atât boții, cât și hackerii:
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);Acest fragment generează dinamic între 1 și 5 câmpuri cu nume aleatorii la fiecare încărcare a paginii.
Posibilitățile sunt aproape nelimitate. Tot ce aveți nevoie este imaginație.
Concluzii
Asta e tot! Sper sincer că acest articol a fost revelator și captivant pentru dumneavoastră. Fie că sunteți un dezvoltator experimentat sau abia vă aventurați în lumea site-urilor web cloud scalabile, împărtășirea cunoștințelor este esențială pentru creșterea comunității.
Dacă ați găsit acest articol valoros, v-aș fi recunoscător dacă l-ați distribui colegilor dezvoltatori sau celor interesați de acest subiect. Fiecare distribuire ne lărgește înțelegerea colectivă.
Feedback-ul și întrebările dumneavoastră sunt foarte apreciate. Nu ezitați să lăsați un comentariu sau să mă contactați – hai să menținem conversația vie. Codare sigură!
Comentarii