Stephan Böni - Kekse

Kekse

Cookie vs. Local Storage

Wenn man die inzwischen überall bekannten Cookie-Warnungen abgenickt hat, möchte man in der Regel nicht mehr damit belästigt werden. Auch dann nicht, wenn man die Seite Tage später erneut besucht. Die herkömmliche Lösung hierfür ist - wer hätte es gedacht - ein Cookie! Doch ist das wirklich der richtige Ansatz?

Wir befassen uns in diesem Beitrag mit den Vor- und Nachteilen von Cookies gegenüber dem Local Storage und zeigen, wie du den Local Storage als Cookie-Alternative verwenden kannst.

Gesetzliche Vorschriften

Befassen wir uns zuerst mit der rechtlichen Situation, die in der EU über die Datensschutzgrundverordnung (DSGVO) geregelt wird. Cookie-Banner bzw. Consent-Banner sind damit Pflicht für alle, welche die Regelungen des Gesetzgebers einhalten wollen. Theoretisch wäre dies für rein technisch notwendige Cookies zwar nicht erforderlich. In der Realität ist die Abgrenzung aber schwierig. Auch ändert sich daran nichts, wenn man statt Cookies den Local Storage nutzt.

Zudem ist seit Anfang 2024 der Website-Betreiber verpflichtet, für jeden Besucher ein Logbuch zu führen, wann dieser seine Einwilligung erteilt und gegebenenfalls auch wieder widerrufen hat. Daher habe ich festgehalten, dass du hier die Cookies am akzeptiert hast.

Wo liegen die Daten?

Ein Cookie wird bei jedem Request an dem Server gesendet. Daher darf es maximal 4 KB Daten beinhalten. Üblicherweise beinhaltet es einen eindeutigen Wert. Das ist ein sogenannter Token, über den der Besucher identifiziert werden kann. Die effektiven Daten, die zu diesem Token passen, liegen nur auf dem Server. Falls also ein Javascript beispielsweise das obige Datum wissen muss, ist ein Server-Request erforderlich.

Im Local Storage können neben dem Token noch weitere Informationen festgehalten werden. Falls aber der Server den Token oder auch einen anderen Wert benötigt, muss dieser ihm explizit übergeben werden.

Vor- und Nachteile

Beide Varianten haben also ihre Vor- und Nachteile. Fassen wir diese zusammen.

Cookies

Vorteile: Legacy-Unterstützung (gibt es schon ewig), Dauerhafte Daten, Verfallsdaten. Cookies können als HTTPOnly markiert werden, was XSS-Angriffe auf den Browser des Benutzers während seiner Sitzung einschränken kann (garantiert aber keine vollständige Immunität gegen XSS-Angriffe).

Nachteile: Jede Domain speichert alle ihre Cookies in einer einzigen Zeichenkette, was das Parsen der Daten erschweren kann. Die Daten sind nicht verschlüsselt. Cookies sind zwar klein, werden aber mit jeder HTTP-Anfrage gesendet. Begrenzte Größe (4 KB).

Local Storage

Vorteile: Wird von den meisten modernen Browsern unterstützt. Dauerhafte Daten, die direkt im Browser gespeichert werden. Für lokal gespeicherte Daten gelten die Regeln des gleichen Ursprungs. Wird nicht mit jeder HTTP-Anfrage gesendet. Etwa 5 MB Speicherplatz pro Domain.

Nachteile: Wenn der Server gespeicherte Client-Informationen benötigt, müssen diese gesendet werden.

Local Storage nutzen

Ich habe mich zur Verwaltung des Consent-Banners für die Variante Local Storage entschieden.

Ein kleines Javascript prüft die Cookie-Einwilligung und mit dem Übergabewert 'accepted' wird diese Einwilligung erteilt. Ob du diese Einwilligung per onclick="cookies('accepted');" direkt im HTML-Button oder mit einem zusätzlichen Event-Handler auslöst, sei dir überlassen.

default.js // Check and Accept Cookies function cookies(action) { if (localStorage.getItem('userCredentials') !== null) { const userCredentialsString = localStorage.getItem('userCredentials'); userCredentials = JSON.parse(userCredentialsString); const currentDate = new Date(userCredentials['cookie_accepted']); return currentDate.toLocaleString('de-CH'); } if (action != 'accepted') { document.getElementById('dialogCookies').showModal(); return; } createUserCredentials('cookie_accepted', new Date.now()); } // Do on DOM Ready document.addEventListener("DOMContentLoaded", (event) => { cookies('check'); });

Die default.js wird ihrerseits in jeder HTML-Seite möglichst weit unten aufgerufen, z.B. direkt vor dem </body>-Tag.

In obigem Code wird also bei jedem Laden einer Seite geprüft, ob die Cookie-Einwilligung erteilt ist. Falls ja, ist dies im Local Storage festgehalten und wir geben das Datum der Einwilligung zurück. Falls nein, zeigen wir den Consent-Banner oder (bei Übergabewert 'accepted') erstellen wir über die Funktion createUserCredentials() den erforderlichen Eintrag im Local Storage.

default.js // Add User Credentials async function createUserCredentials(key, value) { // create token async function generateKey() { const algoritm = { name: "AES-CBC", length: 256 }; const exportable = true; const usage = ['encrypt']; return await window.crypto.subtle.generateKey(algoritm, exportable, usage).then(key => { return key; }); } async function exportKey(key) { const format = "jwk"; return await window.crypto.subtle.exportKey(format, key).then(key => { return key; }); } // sync local storage to server async function syncCredentials(userCredentials) { return await fetch('/includes/userCredentials.php', { method: 'POST', mode: "same-origin", credentials: "same-origin", headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(userCredentials) }) .then(response => { if (!response.ok) { console.log('https connection failed'); } else { return response.json(); } }) .then((data) => { if (!data.success) { console.log(data.error); } }) .catch(error => { console.error('Error:', error); }) } // add local storage entry let userCredentials = {} if (localStorage.getItem('userCredentials') !== null) { const userCredentialsString = localStorage.getItem('userCredentials'); userCredentials = JSON.parse(userCredentialsString); } else { userCredentials.agent = window.navigator.userAgent; const key = await generateKey(); const data = await exportKey(key); userCredentials.token = data['k']; } userCredentials[key] = value; localStorage.setItem('userCredentials', JSON.stringify(userCredentials)); syncCredentials(userCredentials) return userCredentials; }

Hier wird also der Übergabewert dem Array userCredentials im Local Storage hinzugefügt. Falls das Array noch fehlt, wird es mit den initialen Werten zuerst angelegt. Initial benötige ich einen Token zur eindeutigen Identifizierung. Weitere statistische Angaben, wie beispielsweise der User-Agent können ergänzt werden. Zuletzt wird dann das gesamte Array als Backup dem Server mitgeteilt.

Variante mit Klassenmodul

Damit ist alles getan. Falls du jedoch vermeiden willst, dass die default.js überbordet, sollten eher seltener benötigte Aktionen in Klassenmodule ausgelagert werden. Klassenmodule müssen bei Bedarf in der default.js importiert werden.

default.js // Load Module async function loadModule(module) { const file = './modules/' + module.charAt(0).toLowerCase() + module.slice(1) + '.js'; const mod = await import(file); return new mod[module]; }

Ich sammle die Klassenmodule im Unterordner modules und nenne die Dateien wie die Klasse, aber mit Kleinbuchstaben beginnend. Die Klasse UserCredentials befindet sich also in modules/userCredentials.js. Du kannst diese Datei gerne ebenso einbinden.

In der default.js ist die Funktion cookies() leicht anzupassen.

default.js // Check and Accept Cookies async function cookies(action) { const UserCredentials = await loadModule('UserCredentials'); if (action == 'accepted') { UserCredentials.setValue('cookie_accepted', Date.now()); } else { const response = UserCredentials.getValue('cookie_accepted'); if (response.value === undefined) { document.getElementById('dialogCookies').showModal(); } else { return response.value; } } }

Web Push Registrierung

Falls du meine vorgeschlagende Web Push Notification-Einbindung nutzen willst, kannst du dessen Subscription auch im Local Storage hinterlegen, wo sie mit dem Server automatisch synchronisiert wird. Hierfür musst du den gesamten Abschnitt fetch('/includes/register.php', { ... }); mit der Zeile createUserCredentials('push_subscription', subscription); ersetzen.

Dran bleiben

Du hast es geschafft. Abonniere meine Benachrichtigungen, um weitere News und Anleitungen von mir zu erhalten.

Feed einbinden