In der sich wandelnden Landschaft der Webentwicklung hinterlassen serverlose Anwendungen zunehmend ihre Spuren. Ihre unbestreitbaren Vorteile, wie unübertroffene Skalierbarkeit und robuste Leistung, lassen sie herausstechen. Die Schönheit von Serverless liegt in seinem Versprechen: leistungsstarke Lösungen zu liefern, die nicht nur mühelos skalieren, sondern auch budgetfreundlich sind.

Haben Sie jemals davon geträumt, eine Geschäftsidee zu starten, wurden aber von den Technikfragen rund um die Erstellung eines Benutzerregistrierungs- und Anmeldesystems abgeschreckt? Obwohl Plattformen wie Netlify und verschiedene cloudbasierte Datenbanken existieren, können sie nicht mit den Fähigkeiten von Cloudflare mithalten. Viele Alternativen können bei der Skalierung die Kosten in die Höhe treiben, aber mit Cloudflare Pages sieht das anders aus.

Ich habe in den letzten Monaten immer wieder an diesem Projekt und dieser Demo gearbeitet und es neben meinen anderen Verpflichtungen jongliert. Entschuldigung für die Wartezeit, besonders an diejenigen, die sehnsüchtig darauf gewartet haben auf dieses System.

Entfesseln Sie die Kraft von Cloudflare Pages

  • Nahtlose Skalierbarkeit: Verwalten Sie unbegrenzte Benutzerregistrierungen ohne Probleme.
  • Kosteneffizienz: Verabschieden Sie sich von unerwarteten Gemeinkosten; genießen Sie konsistente Preise.
  • Blitzschnelle Geschwindigkeiten: Erleben Sie Leistung wie nie zuvor.
  • Solide Sicherheit: Die Daten Ihrer Benutzer bleiben geschützt und sicher.

Was wir verwenden werden

  • Cloudflare Pages
  • Cloudflare Pages Functions
  • Cloudflare Workers KV

Was ist Cloudflare Pages?

Cloudflare Pages ist eine moderne, benutzerfreundliche Plattform für Entwickler, um ihre Websites zu erstellen, bereitzustellen und zu hosten. Sie bietet eine nahtlose Integration mit GitHub, was bedeutet, dass Sie einfach Ihren Code auf GitHub pushen können und Cloudflare Pages den Rest übernimmt – Build, Deployment und sogar Updates.

So funktioniert es:

  1. Integrierter Workflow: Cloudflare Pages basiert auf dem Git-Workflow. Sobald Sie Ihr GitHub-Repository mit Cloudflare Pages verbinden, beginnt es, Ihre Website bei jedem Push auf den ausgewählten Branch zu bauen und bereitzustellen.
  2. JAMstack-optimiert: Cloudflare Pages unterstützt JAMstack-Prinzipien, was bedeutet, dass Sie Ihre Website mit Ihrem bevorzugten Static-Site-Generator oder JavaScript-Framework erstellen können, einschließlich, aber nicht beschränkt auf Jekyll, Hugo, Next.js und React.
  3. Schnelle und sichere Auslieferung: Angetrieben durch das global verteilte Cloudflare-Netzwerk stellt Pages sicher, dass Ihre Website verfügbar und schnell ist, egal wo sich Ihr Publikum befindet. Außerdem schützen die integrierten Sicherheitsfunktionen von Cloudflare Ihre Website vor Bedrohungen.
  4. Continuous Deployment: Cloudflare Pages baut und stellt Ihre Website automatisch bereit, jedes Mal wenn Sie Aktualisierungen an Ihrem GitHub-Repository vornehmen. Dies ermöglicht schnelle Iterationen und macht den Deployment-Prozess zum Kinderspiel.
  5. Eigene Domain und HTTPS: Mit Pages können Sie eine eigene Domain mit Ihrer Website verbinden, und es bietet kostenloses, automatisches HTTPS für alle Websites, um sicherzustellen, dass die Verbindung stets sicher ist.
  6. Preview Deployments: Immer wenn Sie einen neuen Pull Request in Ihrem verknüpften GitHub-Repository erstellen, generiert Cloudflare Pages automatisch eine einzigartige Preview-URL, mit der Sie Ihre Änderungen vor dem Live-Gang überprüfen können.

Ob Sie ein einzelner Entwickler oder Teil eines großen Teams sind, Cloudflare Pages bietet einen einfachen, schnellen und sicheren Weg, Ihre Websites online zu bringen.

In Anbetracht des oben Gesagten habe ich mich für dieses Benutzerregistrierungssystem für reine und unkomplizierte HTML-Seiten entschieden und auf zusätzliche Frameworks oder Build-Tools verzichtet. Dieser Ansatz gewährleistet unübertroffene Einfachheit und bietet die Flexibilität, jedes gewünschte Ergebnis zu erzielen.

Was ist Cloudflare Workers?

Cloudflare Workers ist eine innovative serverlose Computing-Plattform, die es Entwicklern ermöglicht, ihren Code direkt in das umfangreiche Netzwerk von Cloudflare bereitzustellen, das über 200 Städte weltweit umfasst. Im Wesentlichen ermöglicht es Anwendungen, so nah wie möglich an den Endnutzern ausgeführt zu werden, wodurch Latenzzeiten reduziert und die Benutzererfahrung verbessert werden.

Hier ist ein Überblick über die Funktionen und Vorteile:

  1. Serverlose Ausführungsumgebung: Cloudflare Workers arbeitet in einer serverlosen Umgebung, was bedeutet, dass Entwickler keine Server verwalten oder warten müssen. Stattdessen können sie sich auf das Schreiben ihres Codes konzentrieren, während die Plattform den Rest erledigt, von der Verteilung bis zur Skalierung.
  2. Edge Computing: Im Gegensatz zu herkömmlichen Modellen, bei denen Anwendungen auf einem einzelnen Server oder Rechenzentrum laufen, bringt Cloudflare Workers Ihren Code an den Rand des Cloudflare-Netzwerks. Dies stellt sicher, dass Ihre Anwendung näher am Benutzer läuft und eine verbesserte Leistung und Geschwindigkeit bietet.
  3. Sprachflexibilität: Workers verwendet die V8-JavaScript-Engine, dieselbe Laufzeitumgebung wie Chrome, die es Entwicklern ermöglicht, Code in JavaScript zu schreiben. Darüber hinaus können dank der WebAssembly-Unterstützung auch andere Sprachen wie Rust, C und C++ verwendet werden.
  4. Sicherheit: Durch die Nutzung der inhärenten Sicherheit des Cloudflare-Netzwerks helfen Workers, Anwendungen vor verschiedenen Bedrohungen wie DDoS-Angriffen zu schützen.

Cloudflare Workers bieten eine innovative und hoch skalierbare Lösung für Entwickler, die die Leistung, Zuverlässigkeit und Sicherheit ihrer Anwendungen verbessern möchten.

Innerhalb von Cloudflare Pages sind Workers in einem Verzeichnis namens functions untergebracht. Ich habe meinen gesamten JavaScript/TypeScript-Code in diesem Bereich platziert und dabei die umfassenden Fähigkeiten genutzt, die Workers bieten.

Was ist Cloudflare Workers KV?

Cloudflare Workers KV (Key-Value) ist ein global verteiltes, eventuell konsistentes Key-Value-Speichersystem, mit dem Sie Daten überall in Ihren Cloudflare Workers-Skripten speichern und darauf zugreifen können. Es ist darauf ausgelegt, Ihnen bei der Skalierung und Vereinfachung der Zustandsverwaltung in serverlosen Umgebungen zu helfen.

Hier sind die wichtigsten Funktionen und Vorteile:

  1. Globale Verteilung: Cloudflare Workers KV basiert auf dem Cloudflare-Netzwerk, das über 300 Städte weltweit umfasst. Dies stellt sicher, dass Ihre Daten nahe bei Ihren Benutzern gespeichert und abgerufen werden, wodurch die Latenz reduziert und die Gesamtleistung Ihrer Anwendungen verbessert wird.
  2. Schnelle Lese- und Schreibvorgänge: Workers KV bietet latenzarmen Datenzugriff, der für verschiedene Anwendungen geeignet ist. Während Schreibvorgänge etwas länger brauchen, um sich global zu verbreiten (normalerweise innerhalb weniger Sekunden), sind Lesevorgänge typischerweise schnell, was es ideal für leseintensive Workloads macht.
  3. Großer Maßstab: Sie können Milliarden von Schlüsseln in einem einzigen Workers KV Namespace speichern, und jeder Schlüssel kann einen Wert von bis zu 25 MB enthalten.
  4. Namespaces: KV-Namespaces sind Container für Ihre Key-Value-Paare. Sie ermöglichen es Ihnen, verschiedene Datensätze innerhalb Ihres Workers KV-Speichers zu trennen, was besonders nützlich sein kann, wenn Sie mehrere Anwendungen oder Umgebungen (wie Staging und Produktion) verwalten.
  5. Eventual Consistency: Workers KV verwendet Eventual Consistency. Das bedeutet, dass Updates Ihrer Daten (Schreibvorgänge) sich global verbreiten und im Laufe der Zeit konsistent werden, was normalerweise eine Frage von wenigen Sekunden ist.

Cloudflare Workers KV bietet eine einzigartige Lösung für die Zustandsverwaltung in serverlosen Umgebungen und stellt Entwicklern ein zuverlässiges, schnelles und global verteiltes Datenspeichersystem zur Verfügung.

Bei der Entwicklung dieses Benutzerregistrierungssystems habe ich strategisch die folgenden Workers KV Namespaces konzipiert:

  • USERS: Dies dient als primärer Speicher für alle Benutzer. Es ist darauf ausgelegt, eine im Wesentlichen unbegrenzte Anzahl von Datensätzen zu verarbeiten.
  • USERS_LOGIN_HISTORY: Ein dedizierter Bereich zur Aufzeichnung von Anmeldeaktivitäten, der es Benutzern ermöglicht, die Sicherheit ihres Kontos regelmäßig zu überprüfen.
  • USERS_SESSIONS: Dieser Namespace erfasst Details über den aktuell angemeldeten Benutzer, einschließlich eindeutiger IDs, Geräte, Standorte und mehr.
  • USERS_SESSIONS_MAPPING: Aufgrund des Eventual-Consistency-Modells von Workers KV kann es Verzögerungen zwischen dem Schreiben in USERS_SESSIONS und dem Überprüfen geben. Dies ist besonders wahrscheinlich, wenn Operationen an verschiedenen Edge-Standorten stattfinden. Um dies zu umgehen, füge ich nach der Validierung die neue Session-UID direkt zu USERS_SESSIONS_MAPPING hinzu, um sicherzustellen, dass sie bereits vor dem Schreiben in USERS_SESSIONS einbezogen wird.
  • USERS_TEMP: Ich habe diesen Namespace als Repository für vorübergehende (temporäre) Links und andere Inhalte verwendet, die ein vorbestimmtes Ablaufdatum haben.

Wir haben Datenbanken mit unbegrenzter Kapazität, automatischer Skalierung und hoher Verfügbarkeit erstellt – Funktionen, die oft nur bei teureren Datenbanken zu finden sind.

Gestaltung des Projekts

Ich wollte etwas Unkompliziertes und Effektives schaffen, ohne auf Drittanbieter-Bibliotheken angewiesen zu sein, und das ist mir gelungen. So sieht die gesamte Projektstruktur aus:

 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

Um Ihnen ein klares Bild der obigen Architektur zu geben, lassen Sie uns eine detaillierte Aufschlüsselung vornehmen:

  • framework: Dieses Verzeichnis beherbergt unseren grundlegenden TypeScript-Code. Alles von Datenmodellen bis hin zu E-Mail-Vorlagen befindet sich hier und gewährleistet einen einheitlichen Ansatz in unserem gesamten System.
  • functions: Hier finden Sie den TypeScript-Code, der speziell für Cloudflare Pages Functions zugeschnitten ist und die Backend-Operationen der Website optimiert.
  • public: Alle unsere öffentlich zugänglichen statischen HTML-Dateien befinden sich in diesem Ordner und bilden die sichtbare Oberfläche für Benutzer.

Einfach gesagt: Wenn Sie zur Seite login.html navigieren, wird Cloudflare Pages aktiv und führt den entsprechenden login.ts-Code aus. Dieses dynamische Zusammenspiel setzt sich auf allen Seiten und ihren zugehörigen Funktionen fort.

Mit diesem Setup können wir nahtlos eine Reihe von Aufgaben bewältigen. Ob Content-Rewriting, Datenverarbeitung oder Datenabruf über Cloudflare Workers KV – alles wird effizient verwaltet.

Aufbau des Benutzerregistrierungssystems

Zu Beginn unserer Reise werden wir zunächst ein Benutzerregistrierungssystem aufbauen. Dies ist die Kernfunktionalität.

  1. Unser erster Schritt besteht darin, ein einfaches HTML-Formular zu entwerfen und in register.html zu platzieren, gefolgt von der Erstellung der Funktion, die für die Verarbeitung der bereitgestellten Daten verantwortlich ist:
 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>

Nachdem das Formular eingerichtet ist, besteht der nächste Schritt darin, die Datenübermittlung mit JavaScript zu verbessern. Während die herkömmliche Formularübermittlung durchaus brauchbar ist, habe ich in meinem Beispiel die Bootstrap 5-Validierung integriert und tendiere daher zu AJAX für das Senden der Daten.

Hier ist ein funktionierendes Beispiel aus meiner 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})();
Sobald auf „Create Account" geklickt wird, werden die Daten per AJAX an /register gesendet und der Code in functions/register.ts wird ausgelöst. Dieser Mechanismus ermöglicht die Verarbeitung sowohl von GET- als auch von POST-Daten. Die Verwaltung von OPTIONS ist zwar möglich, wird aber für diese Diskussion ausgeklammert.

  1. Für Standard-Seitenbesuche oder GET-Ausführungen besteht das Potenzial, eine Vielzahl fortgeschrittener Funktionen einzuführen, wie z.B. CSRF-Schutz. Schauen wir uns das folgende Beispiel an:
 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  

Bei diesem Prozess verwenden wir die HTMLRewriter -Funktion von Cloudflare Workers, um bei jeder Seitenanfrage einen einzigartigen CSRF-Token in das Formular zu erzeugen und einzubetten.

Ich werde meine CSRF-Strategie in den folgenden Abschnitten genauer erläutern. Beachten Sie vorerst, wie ich dynamisch einen einzigartigen, zufälligen CSRF-Code anhänge, um die Sicherheit zu verstärken.

  1. Nun wenden wir uns der POST-Ausführungsphase zu. Hier validieren wir sorgfältig die Eingabedaten, speichern sie sicher und senden dann eine E-Mail an den Benutzer zur Bestätigung oder für weitere Anweisungen. Um Ihnen einen konzeptionellen Überblick zu geben, habe ich eine Pseudocode-Darstellung erstellt:
 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  

In meiner Konfiguration werden die Benutzerdaten nach erfolgreicher Registrierung wie folgt in Workers KV gespeichert:

 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}

Sie können dieses Format anpassen und Felder je nach Bedarf hinzufügen oder entfernen. Ich muss betonen, dass ich keinen vollständigen, kopierfertigen Code bereitstellen werde. Es ist wichtig, in das Verständnis und die praktische Erkundung zu investieren. Nur so kann man sich als Entwickler wirklich weiterentwickeln.

Aufbau des Benutzer-Anmeldesystems

Der Übergang zum Anmeldemechanismus für Ihr Cloudflare Pages-Projekt ist ein Kinderspiel. Wir werden einen ähnlichen Ansatz wie bei der Registrierung verfolgen.

  1. Genau wie wir das Registrierungsformular erstellt haben, gestalten Sie ein kompaktes Formular für den Anmeldeprozess und platzieren Sie es in login.html:
 1<form class="row g-3 needs-validation" id="login" method="POST" novalidate>
 2	<div class="col-sm-12">
 3		<label for="email" class="form-label">Email</label>
 4		<input type="email" class="form-control" name="email" id="email" autocomplete="email" placeholder="Required" required>
 5		<div class="invalid-feedback">You must enter your Email.</div>
 6	</div>
 7	<div class="col-sm-12">
 8		<label for="password" class="form-label">Password</label>
 9		<input type="password" class="form-control" name="password" id="password" autocomplete="password" placeholder="Required" required>
10		<div class="invalid-feedback">You must enter a Password.</div>
11	</div>
12	<div class="d-grid gap-2 col-sm-6 mx-auto">
13		<button class="btn btn-primary" type="submit">Login</button>
14	</div>
15</form>
  1. Nach dem Einrichten des Formulars ist es an der Zeit, dessen Rendering zu verarbeiten. Der nächste Schritt beinhaltet die Erstellung des erforderlichen Codes. Dieser wird automatisch aus login.ts ausgelöst:
 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. Der abschließende Schritt ist die Formulierung des Codes zur Verwaltung der POST-Ausführung, der sich in login.ts befindet:
 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  

Durch diese 3 optimierten Schritte haben wir nahtlos das Anmeldesystem aufgebaut. Zweifellos ist der Anmeldemechanismus das komplexeste Segment, das mehrere Operationen wie annotiert erfordert. Betrachten Sie dies jedoch nicht als Komplikation, sondern als eine belebende Herausforderung, die es zu meistern gilt!

Aufbau des Passwort-Zurücksetzungssystems

  1. Ähnlich wie bei unseren vorherigen Komponenten beginnen wir mit der Gestaltung des dedizierten Formulars zum Initiieren einer Passwort-Zurücksetzung:
 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>

Das zugrunde liegende Konzept ist einfach: Wenn eine E-Mail-Adresse eines Benutzers eingegeben wird und sie in unserem System erkannt wird, wird ein einmaliger Link zum Zurücksetzen des Passworts generiert und versendet. Für erhöhte Sicherheit sollte dieser Link nur für eine kurze Dauer aktiv bleiben, idealerweise nicht länger als 2 Stunden.

  1. Beginnen Sie mit der Erstellung des Codes für die Verwaltung der GET-Ausführung, den Sie in forgot-password.ts einbetten möchten:
 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. Fahren Sie fort, indem Sie den Code für die POST-Ausführung formulieren und sicherstellen, dass er in derselben Datei forgot-password.ts untergebracht ist:
 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  

Es liegt nun an Ihnen, das Coding-Puzzle zu vervollständigen. Indem Sie den Hinweisen in meinen Kommentaren folgen, sollten Sie den Prozess recht gut bewältigen können. Sobald fertig, verfügen Sie über ein voll funktionsfähiges System zum Zurücksetzen von Passwörtern.

Warnhinweis: Vermeiden Sie die Anzeige von Nachrichten wie „Diese E-Mail existiert nicht." Solche Hinweise sind Goldgruben für Hacker und ebnen den Weg für potenzielle Brute-Force-Angriffe. Verfolgen Sie stattdessen einen mehrdeutigeren, aber benutzerfreundlichen Ansatz: „Sollte die E-Mail in unserem System registriert sein, erhalten Sie einen Link zum Zurücksetzen."

Demo der Benutzerregistrierung

Erkunden Sie eine Live-Demo des von mir eingerichteten Benutzerregistrierungsprozesses: https://members.mecanik.dev/

Zu den Funktionen gehören:

  • Registrierung, Anmeldung und Passwort-Zurücksetzung
  • Zugang zu Ihrem Profil
  • Verwaltung von Sicherheitseinstellungen und aktiven Sitzungen - Sie können sich beispielsweise trennen, wenn Sie sich von Ihrem Mobilgerät angemeldet haben.
  • Vorschau auf kostenlose Software und Ausschau nach kommenden Premium-Angeboten.

Ich jongliere derzeit mit anderen Projekten, werde diese Plattform aber weiter verbessern. Testen Sie es gerne – stellen Sie nur sicher, dass Sie echte E-Mails verwenden, um meine SendGrid-Reputation nicht zu beeinträchtigen.

Ich freue mich über jedes Feedback und gemeldete Probleme. Bitte melden Sie sich, wenn Sie welche haben!

Fragen und Antworten

Zweifellos steht die Sicherheit an oberster Stelle. Sie könnten die Robustheit des Systems gegenüber potenziellen Angriffen und Datenverletzungen in Frage stellen.

Bedenken Sie, dass Cloudflare Pages auf der Cloudflare-Plattform betrieben wird und Ihnen Zugang zu deren fortschrittlicher Web Application Firewall (WAF) und einer Reihe von Sicherheitstools gewährt.

Wie CSRF tatsächlich funktioniert?

Vielleicht haben Sie die dynamische Generierung und Injektion eines CSRF-Tokens in das Formular bei jedem Laden beobachtet. So stellt dieser Mechanismus die Sicherheit von Anfragen sicher:

Komponenten des CSRF-Tokens:

  • IP: Erfasst die aktuelle IP-Adresse des Benutzers.
  • Country: Identifiziert den aktuellen Standort des Benutzers.
  • UserAgent: Zeichnet die Browserdetails des Benutzers auf.
  • expiry: Setzt einen Timer, indem eine Minute zur aktuellen Zeit addiert wird.

Diese Daten werden im JSON-Format zusammengestellt: {i, c, u, e}, das anschließend verschlüsselt und in Hex umgewandelt wird:

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

Über die Verschlüsselungsfunktion:

  1. Die Daten werden mithilfe eines benutzerdefinierten Passworts verschlüsselt, wobei ein Passwortschlüssel generiert und anschließend ein AES-Schlüssel daraus abgeleitet wird.
  2. Dieser AES-Schlüssel verschlüsselt die Daten, wobei Salt und IV (Initialisierungsvektor) spontan generiert werden.
  3. Die verschlüsselte Ausgabe ist ein ArrayBuffer, der die Iterationsanzahl, Salt, IV und verschlüsselte Daten enthält.

Einfach ausgedrückt, hält dieser Verschlüsselungsprozess branchenübliche Praktiken ein, um die verschlüsselten Daten sowohl unentschlüsselbar als auch vor Manipulation geschützt zu machen.

Validierung des CSRF-Tokens:

 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}

Der Validierungsprozess prüft den CSRF-Token rigoros und stellt sicher, dass er:

  • Von derselben IP-Adresse stammt.
  • Aus demselben Land gesendet wird.
  • Vom selben Browser gesendet wird.
  • Innerhalb seines aktiven Zeitrahmens eingereicht wird.
  • Sollte eine dieser Prüfungen fehlschlagen, identifiziert das System den CSRF-Token als ungültig und bietet robusten Schutz gegen potenzielle Bedrohungen.

Sollte eine dieser Prüfungen fehlschlagen, identifiziert das System den CSRF-Token als ungültig und bietet robusten Schutz gegen potenzielle Bedrohungen.

Passwort-Hashing/Verschlüsselung

Für das Passwort-Hashing/die Verschlüsselung habe ich PBKDF2 (Password-Based Key Derivation Function 2) in Kombination mit dem SHA-256 Hashing-Algorithmus verwendet.

Die Methode verwendet für jedes Passwort ein einzigartiges, kryptographisch sicheres pseudozufälliges „Salt", um erhöhte Sicherheit zu gewährleisten. Dieser Ansatz bietet Schutz gegen Rainbow-Table- und Brute-Force-Angriffe.

Die gehashte Version des Passworts wird gespeichert, nicht der Klartext, was die Datenintegrität weiter stärkt.

Warum keine JWT-Token-Verwendung?

JWT (JSON Web Tokens) sind eine beliebte Methode zur Handhabung der Benutzerauthentifizierung und zur Übertragung von Informationen zwischen Parteien in einem kompakten, URL-sicheren Format.

Obwohl sie viele Vorteile haben, gibt es Gründe, warum man sie in bestimmten Kontexten, wie einem vollständigen Benutzerregistrierungssystem, möglicherweise nicht verwenden möchte. Hier ein tieferer Einblick:

  1. Zustandslosigkeit und Widerruf: Eines der Kennzeichen von JWTs ist ihre Zustandslosigkeit. Doch genau diese Eigenschaft kann für Benutzerverwaltungssysteme problematisch sein. Wenn beispielsweise ein JWT-Token eines Benutzers gestohlen oder kompromittiert wird, gibt es keinen einfachen Weg, diesen Token zu widerrufen, es sei denn, man führt eine Blacklist von Token, was den Zweck der Zustandslosigkeit zunichtemacht.
  2. Größe: Je mehr Daten Sie einem JWT hinzufügen, desto größer wird er. Wenn Sie ein umfassendes Benutzerregistrierungssystem haben, in dem Sie möglicherweise mehr benutzerbezogene Daten im Token speichern müssen, kann dies zu größeren HTTP-Headern und erhöhter Latenz führen.
  3. Speichersicherheit: Für die clientseitige Speicherung von JWTs gehören übliche Speicherorte der Local Storage oder Cookies. Local Storage ist anfällig für XSS-Angriffe, und obwohl Cookies in größerem Umfang gesichert werden können, eröffnen sie Möglichkeiten für CSRF-Angriffe, wenn sie nicht korrekt behandelt werden.
  4. Ablaufbehandlung: Die Verwaltung des Ablaufs von JWTs kann komplex sein. Während kurzlebige Token das Risiko reduzieren, erfordern sie einen Refresh-Mechanismus, der mehr Komplexität in den Authentifizierungsablauf einbringt.
  5. Kein eingebauter Widerruf: Wie bereits erwähnt, gibt es bei Kompromittierung eines Tokens keinen inhärenten Mechanismus, ihn zu widerrufen oder ungültig zu machen, bis er abläuft. Dies kann ein erhebliches Sicherheitsrisiko darstellen, wenn der Token eine lange Lebensdauer hat.
  6. Komplexität für Entwickler: Für diejenigen, die mit JWTs nicht vertraut sind, gibt es eine Lernkurve beim Verständnis, wie man sie korrekt generiert, validiert und verwendet. Diese Komplexität kann Fehler und Schwachstellen einführen, wenn sie nicht gründlich verstanden und implementiert werden.

Bei meiner Erkundung verschiedener Systeme und deren Handhabung von Benutzerdaten stellte ich fest, dass einige Unternehmen eine Fülle von Informationen in ihren JWT-Token speichern, einschließlich sensibler Details wie Benutzerrollen und anderer Daten.

Um es klar zu sagen: Meine Absicht ist es nicht, andere Ansätze zu kritisieren oder zu verunglimpfen. Basierend auf meiner professionellen Haltung zur Sicherheit wirft das Einbetten solch kritischer Informationen direkt in Token jedoch Bedenken auf. Es gibt ein inhärentes Risiko, mehr Daten als nötig offenzulegen, insbesondere wenn diese Daten potenziell ausgenutzt werden könnten, um Benutzerrechte oder andere Funktionalitäten abzuleiten.

Angesichts dieser Vorbehalte habe ich die bewusste Entscheidung getroffen, von dieser Methode abzuweichen. Stattdessen habe ich mich für die bewährte serverseitige Authentifizierung in Kombination mit sicherer Cookie-Handhabung entschieden. Dieser Ansatz schafft meiner Meinung nach ein Gleichgewicht zwischen Benutzererfahrung und robuster Sicherheit und stellt sicher, dass sensible Daten geschützt bleiben und das System widerstandsfähig gegen potenzielle Schwachstellen ist.

Zuverlässiger E-Mail-Versand

Ich habe mich für SendGrid mit dynamischen Vorlagen entschieden, angesichts seiner weiten Verbreitung und meiner tiefen Vertrautheit mit seinen Feinheiten. Um tiefer einzutauchen, verweise ich auf die API-Dokumentation .

Obwohl die Integration einige Komplexitäten aufweisen kann, ist dies Teil der lohnenden Herausforderung. Hier ist ein Beispiel für Sie:

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

Wie kann man die Sicherheit weiter erhöhen?

Obwohl Cloudflare Workers KV gespeicherte Daten verschlüsselt, können Sie eine zusätzliche Verschlüsselungsebene für Benutzerdaten hinzufügen.

Oder möchten Sie potenziellen Angreifern eine Überraschung bereiten? Erwägen Sie die Implementierung von Dummy-Formulareingaben, um Bots und Hacker gleichermaßen zu verwirren:

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

Dieses Snippet generiert dynamisch zwischen 1 und 5 zufällig benannte Eingabefelder bei jedem Seitenaufruf.

Die Möglichkeiten sind nahezu unbegrenzt, Sie müssen nur Ihre Vorstellungskraft einsetzen.

Zusammenfassung

Und da haben wir es! Ich hoffe aufrichtig, dass dieser Beitrag für Sie sowohl aufschlussreich als auch ansprechend war. Ob Sie ein erfahrener Entwickler sind oder gerade erst in die Welt der skalierbaren Cloud-Websites eintauchen – das Teilen von Wissen ist für das Wachstum unserer Community von entscheidender Bedeutung.

Wenn Sie diesen Artikel wertvoll fanden, erwägen Sie bitte, ihn an Entwicklerkollegen oder andere Interessierte weiterzugeben. Jedes Teilen erweitert unser kollektives Verständnis.

Ihr Feedback und Ihre Fragen sind überaus geschätzt. Zögern Sie nicht, einen Kommentar zu hinterlassen oder sich zu melden – lassen Sie uns das Gespräch fortsetzen. Sicheres Programmieren!