Implementierung von
Wenhao Wang
Dev Intern · Leapcell

Einleitung
In der schnelllebigen Welt der Webanwendungen ist Benutzerfreundlichkeit von größter Bedeutung. Ein Feature, das diesen Komfort erheblich verbessert, ist "Remember Me", das es Benutzern ermöglicht, über Browsersitzungen hinweg eingeloggt zu bleiben, ohne wiederholt ihre Anmeldedaten eingeben zu müssen. Obwohl scheinbar unkompliziert, birgt die Implementierung einer sicheren und langlebigen "Remember Me"-Funktionalität interessante Herausforderungen, insbesondere im Hinblick auf den Schutz von Benutzerdaten und die Langlebigkeit der Autorisierung. Traditionelle Ansätze lassen oft zu wünschen übrig, wenn es darum geht, Sicherheit und Persistenz auszubalancieren. Dieser Artikel befasst sich mit einer robusten Lösung für JavaScript-Anwendungen: die Nutzung von Refresh Tokens als langfristige, sichere "Remember Me"-Strategie. Wir werden die zugrunde liegenden Konzepte untersuchen, Implementierungsdetails diskutieren und demonstrieren, wie ein effektives und sicheres System aufgebaut werden kann.
Kernkonzepte erklärt
Bevor wir uns mit der Implementierung befassen, definieren wir einige Schlüsselbegriffe, die das Rückgrat unserer Diskussion bilden werden:
- Access Token: Ein Berechtigungsnachweis, der für den Zugriff auf geschützte Ressourcen verwendet wird. Access Tokens sind typischerweise kurzlebig (Minuten bis Stunden) und werden bei jeder Anfrage gesendet, um APIs abzusichern. Ihre kurze Lebensdauer minimiert das Risiko durch Token-Diebstahl.
- Refresh Token: Ein langlebiger Berechtigungsnachweis, der zur Erlangung neuer Access Tokens verwendet wird, nachdem der aktuelle abgelaufen ist. Im Gegensatz zu Access Tokens werden Refresh Tokens nicht bei jeder API-Anfrage gesendet. Sie werden sicherer gespeichert und seltener verwendet, in der Regel nur, wenn ein Access Token erneuert werden muss.
- JWT (JSON Web Token): Ein kompaktes, URL-sicheres Mittel zur Darstellung von Ansprüchen, die zwischen zwei Parteien übertragen werden. JWTs werden oft für Access Tokens verwendet und enthalten Informationen wie Benutzer-ID, Rollen und Ablaufdatum, die digital signiert sind, um die Integrität zu gewährleisten.
- Session vs. Persistenz: Eine Session bezieht sich typischerweise auf einen temporären, interaktiven Informationsaustausch zwischen zwei oder mehr kommunizierenden Geräten oder Programmen. Persistenz bedeutet in diesem Zusammenhang die Fähigkeit des angemeldeten Status des Benutzers, Browserschließungen oder längere Inaktivitätsperioden zu überstehen.
- HTTP-only Cookie: Eine spezielle Art von Cookie, auf das clientseitiges JavaScript keinen Zugriff hat. Dies macht es immun gegen Cross-Site-Scripting (XSS)-Angriffe, da ein Angreifer den Wert des Cookies nicht einfach auslesen kann. Dies ist eine entscheidende Sicherheitsmaßnahme zur Speicherung sensibler Tokens wie Refresh Tokens.
- CSRF (Cross-Site Request Forgery) Token: Ein geheimer, eindeutiger und unvorhersehbarer Wert, der vom Server generiert und an den Client gesendet wird. Der Client muss diesen Token dann bei nachfolgenden Anfragen mitführen, typischerweise in einem Header oder Formularfeld. Der Server validiert diesen Token und verhindert so unbefugte Anfragen, die von anderen Domains initiiert wurden.
Implementierung von "Remember Me" mit Refresh Tokens
Die Kernidee ist die Verwendung eines kurzlebigen Access Tokens für unmittelbare API-Anfragen und eines langlebigen Refresh Tokens, der sicher gespeichert wird, um bei Bedarf neue Access Tokens auszustellen. Dieser Ansatz bietet eine starke Balance zwischen Sicherheit und Benutzerfreundlichkeit.
Der Ablauf
-
Benutzeranmeldung:
- Der Benutzer gibt Anmeldedaten ein (Benutzername/Passwort).
- Das Backend authentifiziert den Benutzer.
- Bei Erfolg stellt das Backend einen Access Token und einen Refresh Token aus.
- Der Access Token wird typischerweise im Antwortkörper oder in einem Nicht-HTTP-only-Cookie gesendet.
- Der Refresh Token wird als HTTP-only, sicheres Cookie vom Server gesetzt. Dies verhindert, dass JavaScript darauf zugreifen kann, und mindert XSS-Risiken.
- Wenn die Checkbox "Remember Me" aktiviert ist, wird die Ablaufzeit des Refresh Tokens viel länger gesetzt (z. B. 30 Tage bis mehrere Monate). Andernfalls kann es sich um ein Standard-Session-Cookie oder eine kürzere Dauer handeln.
-
Nachfolgende API-Anfragen:
- Der JavaScript-Client fügt den Access Token (z. B. im
Authorization: Bearer <token>
-Header) jeder geschützten API-Anfrage bei.
- Der JavaScript-Client fügt den Access Token (z. B. im
-
Ablauf des Access Tokens:
- Wenn der Access Token abläuft, schlägt eine API-Anfrage mit dem alten Token fehl (z. B. mit einem 401 Unauthorized Statuscode).
- Der clientseitige JavaScript erkennt diesen 401-Fehler.
-
Token-Aktualisierung:
- Beim Erkennen eines abgelaufenen Access Tokens sendet der Client eine Anfrage an einen dedizierten "Token Refresh"-Endpunkt auf dem Server.
- Diese Anfrage sendet automatisch den HTTP-only Refresh Token Cookie an den Server.
- Der Server validiert den Refresh Token:
- Prüft seine Gültigkeit und Ablaufzeit.
- Stellt sicher, dass er nicht widerrufen wurde.
- (Optional, aber empfohlen) Validiert jeden zugehörigen CSRF-Token.
- Bei Gültigkeit stellt der Server einen neuen Access Token und möglicherweise einen neuen Refresh Token aus (Token-Rotation für erhöhte Sicherheit).
- Der neue Access Token wird an den Client gesendet. Wenn ein neuer Refresh Token ausgestellt wird, überschreibt er den alten in einem HTTP-only Cookie.
-
Wiederholung der ursprünglichen Anfrage:
- Mit dem neuen Access Token wiederholt der Client die ursprüngliche fehlgeschlagene API-Anfrage.
Codebeispiele (Konzeptionell: Frontend & Backend)
Lassen Sie uns dies mit vereinfachten JavaScript (Frontend) und Node.js (Backend) Beispielen veranschaulichen.
Frontend (JavaScript - unter Verwendung von axios
für Anfragen)
// Ein einfacher Speicher für den Access Token (in einer echten App, verwenden Sie eine robustere Zustandsverwaltung) let accessToken = null; // Funktion zum Speichern des Access Tokens const setAccessToken = (token) => { accessToken = token; // In einer echten App könnten Sie ihn auch für sofortigen Zugriff nach einem Seiten-Refresh in localStorage speichern // localStorage.setItem('accessToken', token); }; // Funktion zum Abrufen des Access Tokens const getAccessToken = () => { // return accessToken || localStorage.getItem('accessToken'); return accessToken; }; // Axios-Instanz mit Interceptor für die Token-Behandlung const api = axios.create({ baseURL: '/api', }); api.interceptors.request.use( (config) => { const token = getAccessToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); api.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Wenn der Fehler 401 ist und nicht bereits wiederholt wird if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; // Als wiederholt markieren try { // Fordern Sie einen neuen Access Token mit dem Refresh Token an (der sich im HTTP-only Cookie befindet) const refreshResponse = await axios.post('/api/auth/refresh-token'); setAccessToken(refreshResponse.data.accessToken); // Aktualisieren Sie den Header der ursprünglichen Anfrage mit dem neuen Token originalRequest.headers.Authorization = `Bearer ${refreshResponse.data.accessToken}`; // Wiederholen Sie die ursprüngliche Anfrage return api(originalRequest); } catch (refreshError) { // Refresh Token fehlgeschlagen, möglicherweise abgelaufen oder ungültig. // Melden Sie den Benutzer ab oder leiten Sie zum Login weiter. console.error("Refresh Token fehlgeschlagen:", refreshError); // Alle gespeicherten Tokens löschen und zum Login umleiten setAccessToken(null); // localStorage.removeItem('accessToken'); window.location.href = '/login'; return Promise.reject(refreshError); } } return Promise.reject(error); } ); // --- Beispiel für Benutzeranmeldung --- async function loginUser(username, password, rememberMe) { try { const response = await axios.post('/api/auth/login', { username, password, rememberMe, // "Remember Me"-Präferenz senden }); setAccessToken(response.data.accessToken); // Backend setzt den Refresh Token als HTTP-only Cookie console.log("Erfolgreich eingeloggt!"); // Weiterleitung zum Dashboard oder zur Homepage } catch (error) { console.error("Anmeldung fehlgeschlagen:", error.response?.data?.message || error.message); } } // Beispielaufruf: // loginUser('user@example.com', 'password123', true); // api.get('/user/profile').then(res => console.log(res.data));
Backend (Node.js mit Express & JWT)
const express = require('express'); const jwt = require('jsonwebtoken'); const cookieParser = require('cookie-parser'); // Zum Parsen von HTTP-only Cookies const csrf = require('csurf'); // Für CSRF-Schutz const app = express(); app.use(express.json()); app.use(cookieParser()); const JWT_SECRET = 'your_jwt_secret_key'; // Verwenden Sie eine starke, Umgebungsvariable const REFRESH_TOKEN_SECRET = 'your_refresh_token_secret_key'; // Verwenden Sie eine starke, Umgebungsvariable // CSRF-Schutz einrichten, der ein Cookie für das CSRF-Token selbst verwendet const csrfProtection = csrf({ cookie: true }); app.use(csrfProtection); // Global oder für bestimmte Routen anwenden // Dummy-Benutzerdaten const users = [{ id: 1, username: 'user@example.com', password: 'password123' }]; // Hilfsfunktion zum Generieren von Tokens const generateTokens = (user, rememberMe) => { const accessToken = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: '15m' }); // Kurzlebig // Ablaufzeit des Refresh Tokens basierend auf 'rememberMe' const refreshTokenExpiry = rememberMe ? '30d' : '1h'; // 30 Tage vs. 1 Stunde const refreshToken = jwt.sign({ userId: user.id }, REFRESH_TOKEN_SECRET, { expiresIn: refreshTokenExpiry }); return { accessToken, refreshToken }; }; // Middleware zur Überprüfung des Access Tokens const authenticateAccessToken = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) return res.status(401).json({ message: 'Kein Access Token vorhanden' }); const token = authHeader.split(' ')[1]; if (!token) return res.status(401).json({ message: 'Token-Format ist Bearer <token>' }); jwt.verify(token, JWT_SECRET, (err, user) => { if (err) return res.status(401).json({ message: 'Ungültiger oder abgelaufener Access Token' }); req.user = user; next(); }); }; // --- AUTH ENDPUNKTE --- // Login app.post('/api/auth/login', (req, res) => { const { username, password, rememberMe } = req.body; const user = users.find(u => u.username === username && u.password === password); if (!user) { return res.status(401).json({ message: 'Ungültige Anmeldedaten' }); } const { accessToken, refreshToken } = generateTokens(user, rememberMe); // Refresh Token als HTTP-only sicheres Cookie setzen res.cookie('refreshToken', refreshToken, { httpOnly: true, // Von clientseitigem JS nicht zugänglich secure: process.env.NODE_ENV === 'production', // Nur über HTTPS in der Produktion senden sameSite: 'Lax', // CSRF-Schutz - nicht mit Cross-Site-Anfragen senden maxAge: (rememberMe ? 30 * 24 * 60 * 60 * 1000 : 60 * 60 * 1000), // 30 Tage oder 1 Stunde }); // CSRF Token in ein zugängliches Cookie oder Header für den Client zum Lesen setzen const csrfToken = req.csrfToken(); res.cookie('XSRF-TOKEN', csrfToken); // Für clientseitigen Zugriff res.json({ accessToken, csrfToken }); // CSRF Token auch im Antwortkörper senden }); // Token Refresh app.post('/api/auth/refresh-token', csrfProtection, (req, res) => { // CSRF-Schutz anwenden const refreshToken = req.cookies.refreshToken; if (!refreshToken) { return res.status(401).json({ message: 'Kein Refresh Token vorhanden' }); } jwt.verify(refreshToken, REFRESH_TOKEN_SECRET, (err, user) => { if (err) { // Abgelaufenen/ungültigen Refresh Token löschen res.clearCookie('refreshToken'); return res.status(403).json({ message: 'Ungültiger oder abgelaufener Refresh Token' }); } // Token-Rotation: Neuen Refresh Token zusammen mit einem neuen Access Token ausstellen const { accessToken, refreshToken: newRefreshToken } = generateTokens(user, true); // Annahme: 'rememberMe' ist true, wenn Refresh Token verwendet wird res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Lax', maxAge: 30 * 24 * 60 * 60 * 1000, // 30 Tage }); const csrfToken = req.csrfToken(); res.cookie('XSRF-TOKEN', csrfToken); res.json({ accessToken, csrfToken }); }); }); // Logout (Refresh Token widerrufen, falls in DB gespeichert) app.post('/api/auth/logout', (req, res) => { // In einer echten App möchten Sie vielleicht den Refresh Token aus einer Datenbank widerrufen res.clearCookie('refreshToken'); res.clearCookie('XSRF-TOKEN'); // CSRF Token ebenfalls löschen res.status(200).json({ message: 'Erfolgreich abgemeldet' }); }); // --- GESCHÜTZTE ROUTEN --- // Beispiel für geschützte Route app.get('/api/user/profile', authenticateAccessToken, (req, res) => { res.json({ message: `Willkommen, Benutzer ${req.user.userId}! Ihre Profildaten hier.` }); }); app.listen(3000, () => console.log('Server läuft auf Port 3000'));
Sicherheitsaspekte
- HTTP-only Cookies für Refresh Tokens: Dies ist entscheidend. Es verhindert, dass JavaScript auf den Refresh Token zugreifen kann, was ihn für XSS-Angriffe unempfindlich macht, bei denen ein Angreifer bösartige Skripte einschleusen könnte, um Cookies zu stehlen.
Secure
-Flag für Cookies: Stellen Sie sicher, dass dassecure
-Flag in Produktionsumgebungen auftrue
gesetzt ist. Dies stellt sicher, dass der Cookie nur über HTTPS-Verbindungen gesendet wird und schützt ihn vor Lauschangriffen.SameSite
-Flag für Cookies: Das Setzen desSameSite
-Attributs (z. B.Lax oder
Strict`) für den Refresh Token Cookie hilft, CSRF-Angriffe zu mindern, indem Browser angewiesen werden, den Cookie nicht mit Cross-Site-Anfragen zu senden.- CSRF-Schutz für den Refresh-Endpunkt: Auch mit
SameSite
ist es eine bewährte Methode, zusätzlichen CSRF-Schutz für Ihren/refresh-token
-Endpunkt zu implementieren. Dies beinhaltet typischerweise einen CSRF-Token, der in einem regulären (nicht-HTTP-only) Cookie oderlocalStorage
gespeichert ist, den der Client mit der Refresh-Anfrage sendet und den der Server validiert. - Token-Widerruf: Implementieren Sie einen Mechanismus zum Widerrufen von Refresh Tokens (z. B. Speichern in einer Datenbank und Kennzeichnen als ungültig bei Abmeldung oder Kompromittierung des Kontos). Dies ist entscheidend für die Reaktion auf Sicherheitsvorfälle.
- Token-Rotation: Das Ausstellen eines neuen Refresh Tokens bei jeder erfolgreichen Token-Aktualisierung (und das Ungültigmachen des alten) ist eine bewährte Methode. Wenn ein Angreifer einen Refresh Token stiehlt, wird er nach der nächsten legitimen Aktualisierung nutzlos.
- Ratenbegrenzung: Schützen Sie Ihre Login- und Token-Refresh-Endpunkte vor Brute-Force-Angriffen, indem Sie Ratenbegrenzungen implementieren.
- Umgebungsvariablen: Speichern Sie niemals sensible Geheimnisse (wie JWT-Geheimnisse) direkt in Ihrem Code. Verwenden Sie Umgebungsvariablen.
Anwendungsszenarien
Diese Refresh Token-Strategie ist ideal für:
- Single Page Applications (SPAs): React, Vue, Angular-Anwendungen, bei denen ein dauerhaftes Login ohne vollständige Seitenaktualisierungen erwartet wird.
- Mobile Anwendungen (Hybrid/PWAs): Bietet ein nahtloses Login-Erlebnis für mobile Benutzer.
- Jede clientseitige Anwendung, die einen langlebigen authentifizierten Zustand erfordert und gleichzeitig eine starke Sicherheit aufrechterhält.
Fazit
Die Implementierung einer sicheren "Remember Me"-Funktion ist keine triviale Aufgabe. Durch die sorgfältige Nutzung von Refresh Tokens und die Einhaltung robuster Sicherheitspraktiken können Entwickler sowohl die Benutzererfahrung als auch die Anwendungssicherheit erheblich verbessern. Der Ansatz, kurzlebige Access Tokens mit langlebigen, HTTP-only Refresh Tokens zu koppeln, verstärkt durch Maßnahmen wie Token-Rotation und CSRF-Schutz, bietet eine widerstandsfähige und branchenübliche Lösung für die persistente Authentifizierung in JavaScript-Anwendungen. Er gleicht effektiv den Wunsch nach Benutzerfreundlichkeit mit der Notwendigkeit, sensible Benutzerdaten zu schützen, aus.