Implementierung von Authentifizierung in Next.js
Das Verständnis von Authentifizierung ist entscheidend für den Schutz der Daten Ihrer Anwendung. Diese Seite führt Sie durch die React- und Next.js-Funktionen, die für die Implementierung von Authentifizierung verwendet werden können.
Bevor Sie beginnen, ist es hilfreich, den Prozess in drei Konzepte zu unterteilen:
- Authentifizierung: Überprüft, ob der Benutzer derjenige ist, für den er sich ausgibt. Der Benutzer muss seine Identität mit etwas nachweisen, das er besitzt, wie z.B. einem Benutzernamen und Passwort.
- Sitzungsverwaltung: Verfolgt den Authentifizierungsstatus des Benutzers über Anfragen hinweg.
- Autorisierung: Entscheidet, auf welche Routen und Daten der Benutzer zugreifen kann.
Dieses Diagramm zeigt den Authentifizierungsablauf mit React- und Next.js-Funktionen:

Die Beispiele auf dieser Seite zeigen aus didaktischen Gründen eine grundlegende Authentifizierung mit Benutzername und Passwort. Obwohl Sie eine eigene Authentifizierungslösung implementieren können, empfehlen wir aus Sicherheits- und Einfachheitsgründen die Verwendung einer Authentifizierungsbibliothek. Diese bieten integrierte Lösungen für Authentifizierung, Sitzungsverwaltung und Autorisierung sowie zusätzliche Funktionen wie Social Logins, Multi-Faktor-Authentifizierung und rollenbasierte Zugriffskontrolle. Eine Liste finden Sie im Abschnitt Authentifizierungsbibliotheken.
Authentifizierung
Registrierungs- und Anmeldefunktionalität
Sie können das <form>
-Element mit Server Actions von React und useActionState
verwenden, um Benutzeranmeldedaten zu erfassen, Formularfelder zu validieren und die API oder Datenbank Ihres Authentifizierungsanbieters aufzurufen.
Da Server Actions immer auf dem Server ausgeführt werden, bieten sie eine sichere Umgebung für die Handhabung der Authentifizierungslogik.
Hier sind die Schritte zur Implementierung der Registrierungs-/Anmeldefunktionalität:
1. Benutzeranmeldedaten erfassen
Um Benutzeranmeldedaten zu erfassen, erstellen Sie ein Formular, das beim Absenden eine Server Action aufruft. Beispielsweise ein Registrierungsformular, das den Namen, die E-Mail-Adresse und das Passwort des Benutzers akzeptiert:
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
import { signup } from '@/app/actions/auth'
export function SignupForm() {
return (
<form action={signup}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" type="email" placeholder="Email" />
</div>
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
<button type="submit">Sign Up</button>
</form>
)
}
export async function signup(formData: FormData) {}
export async function signup(formData) {}
2. Formularfelder auf dem Server validieren
Verwenden Sie die Server Action, um die Formularfelder auf dem Server zu validieren. Falls Ihr Authentifizierungsanbieter keine Formularvalidierung bietet, können Sie eine Schema-Validierungsbibliothek wie Zod oder Yup verwenden.
Am Beispiel von Zod können Sie ein Formularschema mit entsprechenden Fehlermeldungen definieren:
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
export type FormState =
| {
errors?: {
name?: string[]
email?: string[]
password?: string[]
}
message?: string
}
| undefined
import { z } from 'zod'
export const SignupFormSchema = z.object({
name: z
.string()
.min(2, { message: 'Name must be at least 2 characters long.' })
.trim(),
email: z.string().email({ message: 'Please enter a valid email.' }).trim(),
password: z
.string()
.min(8, { message: 'Be at least 8 characters long' })
.regex(/[a-zA-Z]/, { message: 'Contain at least one letter.' })
.regex(/[0-9]/, { message: 'Contain at least one number.' })
.regex(/[^a-zA-Z0-9]/, {
message: 'Contain at least one special character.',
})
.trim(),
})
Um unnötige Aufrufe an die API oder Datenbank Ihres Authentifizierungsanbieters zu vermeiden, können Sie in der Server Action frühzeitig return
aufrufen, falls Formularfelder nicht dem definierten Schema entsprechen.
import { SignupFormSchema, FormState } from '@/app/lib/definitions'
export async function signup(state: FormState, formData: FormData) {
// Formularfelder validieren
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// Bei ungültigen Feldern frühzeitig zurückkehren
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Anbieter oder DB aufrufen, um Benutzer zu erstellen...
}
import { SignupFormSchema } from '@/app/lib/definitions'
export async function signup(state, formData) {
// Formularfelder validieren
const validatedFields = SignupFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
password: formData.get('password'),
})
// Bei ungültigen Feldern frühzeitig zurückkehren
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Anbieter oder DB aufrufen, um Benutzer zu erstellen...
}
Zurück in Ihrer <SignupForm />
können Sie den useActionState
-Hook von React verwenden, um Validierungsfehler während des Formularversands anzuzeigen:
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
'use client'
import { signup } from '@/app/actions/auth'
import { useActionState } from 'react'
export default function SignupForm() {
const [state, action, pending] = useActionState(signup, undefined)
return (
<form action={action}>
<div>
<label htmlFor="name">Name</label>
<input id="name" name="name" placeholder="Name" />
</div>
{state?.errors?.name && <p>{state.errors.name}</p>}
<div>
<label htmlFor="email">Email</label>
<input id="email" name="email" placeholder="Email" />
</div>
{state?.errors?.email && <p>{state.errors.email}</p>}
<div>
<label htmlFor="password">Password</label>
<input id="password" name="password" type="password" />
</div>
{state?.errors?.password && (
<div>
<p>Password must:</p>
<ul>
{state.errors.password.map((error) => (
<li key={error}>- {error}</li>
))}
</ul>
</div>
)}
<button disabled={pending} type="submit">
Sign Up
</button>
</form>
)
}
Gut zu wissen:
- In React 19 enthält
useFormStatus
zusätzliche Schlüssel im zurückgegebenen Objekt, wie data, method und action. Falls Sie nicht React 19 verwenden, ist nur derpending
-Schlüssel verfügbar.- Bevor Sie Daten mutieren, sollten Sie immer sicherstellen, dass ein Benutzer auch berechtigt ist, die Aktion auszuführen. Siehe Authentifizierung und Autorisierung.
3. Benutzer erstellen oder Anmeldedaten überprüfen
Nach der Validierung der Formularfelder können Sie ein neues Benutzerkonto erstellen oder überprüfen, ob der Benutzer existiert, indem Sie die API oder Datenbank Ihres Authentifizierungsanbieters aufrufen.
Fortsetzung des vorherigen Beispiels:
export async function signup(state: FormState, formData: FormData) {
// 1. Formularfelder validieren
// ...
// 2. Daten für die Einfügung in die Datenbank vorbereiten
const { name, email, password } = validatedFields.data
// z.B. Passwort des Benutzers vor der Speicherung hashen
const hashedPassword = await bcrypt.hash(password, 10)
// 3. Benutzer in die Datenbank einfügen oder Auth Library API aufrufen
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'Beim Erstellen Ihres Kontos ist ein Fehler aufgetreten.',
}
}
// TODO:
// 4. Benutzersitzung erstellen
// 5. Benutzer weiterleiten
}
export async function signup(state, formData) {
// 1. Formularfelder validieren
// ...
// 2. Daten für die Einfügung in die Datenbank vorbereiten
const { name, email, password } = validatedFields.data
// z.B. Passwort des Benutzers vor der Speicherung hashen
const hashedPassword = await bcrypt.hash(password, 10)
// 3. Benutzer in die Datenbank einfügen oder Library API aufrufen
const data = await db
.insert(users)
.values({
name,
email,
password: hashedPassword,
})
.returning({ id: users.id })
const user = data[0]
if (!user) {
return {
message: 'Beim Erstellen Ihres Kontos ist ein Fehler aufgetreten.',
}
}
// TODO:
// 4. Benutzersitzung erstellen
// 5. Benutzer weiterleiten
}
Nach erfolgreicher Erstellung des Benutzerkontos oder Überprüfung der Anmeldedaten können Sie eine Sitzung erstellen, um den Authentifizierungsstatus des Benutzers zu verwalten. Abhängig von Ihrer Sitzungsverwaltungsstrategie kann die Sitzung in einem Cookie oder einer Datenbank oder beidem gespeichert werden. Fahren Sie mit dem Abschnitt Sitzungsverwaltung fort, um mehr zu erfahren.
Tipps:
- Das obige Beispiel ist ausführlich, da es die Authentifizierungsschritte zu Schulungszwecken aufschlüsselt. Dies zeigt, dass die Implementierung einer eigenen sicheren Lösung schnell komplex werden kann. Erwägen Sie die Verwendung einer Auth Library, um den Prozess zu vereinfachen.
- Um die Benutzererfahrung zu verbessern, können Sie bereits früher im Registrierungsprozess auf doppelte E-Mails oder Benutzernamen prüfen, z.B. während der Benutzer einen Benutzernamen eingibt oder wenn das Eingabefeld den Fokus verliert. Dies kann unnötige Formularübermittlungen verhindern und dem Benutzer sofortiges Feedback geben. Sie können solche Anfragen mit Bibliotheken wie use-debounce entprellen, um die Häufigkeit dieser Prüfungen zu steuern.
Sitzungsverwaltung
Die Sitzungsverwaltung stellt sicher, dass der authentifizierte Zustand des Benutzers über Anfragen hinweg erhalten bleibt. Sie umfasst das Erstellen, Speichern, Aktualisieren und Löschen von Sitzungen oder Tokens.
Es gibt zwei Arten von Sitzungen:
- Zustandslos (Stateless): Die Sitzungsdaten (oder ein Token) werden in den Cookies des Browsers gespeichert. Der Cookie wird mit jeder Anfrage gesendet, was die Überprüfung der Sitzung auf dem Server ermöglicht. Diese Methode ist einfacher, kann aber weniger sicher sein, wenn sie nicht korrekt implementiert wird.
- Datenbank (Database): Die Sitzungsdaten werden in einer Datenbank gespeichert, wobei der Browser des Benutzers nur die verschlüsselte Sitzungs-ID erhält. Diese Methode ist sicherer, kann aber komplexer sein und mehr Serverressourcen verbrauchen.
Gut zu wissen: Während Sie beide Methoden oder auch beide verwenden können, empfehlen wir die Verwendung einer Sitzungsverwaltungsbibliothek wie iron-session oder Jose.
Zustandslose Sitzungen (Stateless Sessions)
Um zustandslose Sitzungen zu erstellen und zu verwalten, müssen Sie einige Schritte befolgen:
- Generieren Sie einen geheimen Schlüssel, der zum Signieren Ihrer Sitzung verwendet wird, und speichern Sie ihn als Umgebungsvariable.
- Schreiben Sie Logik zum Verschlüsseln/Entschlüsseln von Sitzungsdaten mit einer Sitzungsverwaltungsbibliothek.
- Verwalten Sie Cookies mit der Next.js
cookies
API.
Zusätzlich zu den oben genannten Punkten sollten Sie Funktionen hinzufügen, um die Sitzung zu aktualisieren (oder zu erneuern), wenn der Benutzer zur Anwendung zurückkehrt, und die Sitzung zu löschen, wenn der Benutzer sich abmeldet.
Gut zu wissen: Prüfen Sie, ob Ihre Auth Library Sitzungsverwaltung enthält.
1. Generieren eines geheimen Schlüssels
Es gibt mehrere Möglichkeiten, einen geheimen Schlüssel zum Signieren Ihrer Sitzung zu generieren. Beispielsweise können Sie den Befehl openssl
in Ihrem Terminal verwenden:
openssl rand -base64 32
Dieser Befehl generiert eine zufällige 32-stellige Zeichenfolge, die Sie als geheimen Schlüssel verwenden und in Ihrer Umgebungsvariablendatei speichern können:
SESSION_SECRET=your_secret_key
Sie können dann auf diesen Schlüssel in Ihrer Sitzungsverwaltungslogik verweisen:
const secretKey = process.env.SESSION_SECRET
2. Verschlüsseln und Entschlüsseln von Sitzungen
Als Nächstes können Sie Ihre bevorzugte Sitzungsverwaltungsbibliothek verwenden, um Sitzungen zu verschlüsseln und zu entschlüsseln. Fortsetzung des vorherigen Beispiels verwenden wir Jose (kompatibel mit der Edge Runtime) und Reacts server-only
Paket, um sicherzustellen, dass Ihre Sitzungsverwaltungslogik nur auf dem Server ausgeführt wird.
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
import { SessionPayload } from '@/app/lib/definitions'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload: SessionPayload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session: string | undefined = '') {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Sitzungsüberprüfung fehlgeschlagen')
}
}
import 'server-only'
import { SignJWT, jwtVerify } from 'jose'
const secretKey = process.env.SESSION_SECRET
const encodedKey = new TextEncoder().encode(secretKey)
export async function encrypt(payload) {
return new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setExpirationTime('7d')
.sign(encodedKey)
}
export async function decrypt(session) {
try {
const { payload } = await jwtVerify(session, encodedKey, {
algorithms: ['HS256'],
})
return payload
} catch (error) {
console.log('Sitzungsüberprüfung fehlgeschlagen')
}
}
Tipps:
- Die Nutzdaten sollten die minimalen, eindeutigen Benutzerdaten enthalten, die in nachfolgenden Anfragen verwendet werden, wie z.B. die Benutzer-ID, Rolle usw. Sie sollten keine personenbezogenen Informationen wie Telefonnummer, E-Mail-Adresse, Kreditkarteninformationen usw. oder sensible Daten wie Passwörter enthalten.
3. Cookies setzen (empfohlene Optionen)
Um die Sitzung in einem Cookie zu speichern, verwenden Sie die Next.js cookies
API. Der Cookie sollte auf dem Server gesetzt werden und die empfohlenen Optionen enthalten:
- HttpOnly: Verhindert den Zugriff auf den Cookie durch clientseitiges JavaScript.
- Secure: Verwendet https zum Senden des Cookies.
- SameSite: Gibt an, ob der Cookie mit cross-site-Anfragen gesendet werden kann.
- Max-Age oder Expires: Löscht den Cookie nach einem bestimmten Zeitraum.
- Path: Definiert den URL-Pfad für den Cookie.
Weitere Informationen zu diesen Optionen finden Sie unter MDN.
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId: string) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
export async function createSession(userId) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const session = await encrypt({ userId, expiresAt })
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
Zurück in Ihrer Server-Aktion können Sie die Funktion createSession()
aufrufen und die redirect()
API verwenden, um den Benutzer zur entsprechenden Seite weiterzuleiten:
import { createSession } from '@/app/lib/session'
export async function signup(state: FormState, formData: FormData) {
// Vorherige Schritte:
// 1. Formularfelder validieren
// 2. Daten für die Einfügung in die Datenbank vorbereiten
// 3. Benutzer in die Datenbank einfügen oder Library API aufrufen
// Aktuelle Schritte:
// 4. Benutzersitzung erstellen
await createSession(user.id)
// 5. Benutzer weiterleiten
redirect('/profile')
}
import { createSession } from '@/app/lib/session'
export async function signup(state, formData) {
// Vorherige Schritte:
// 1. Formularfelder validieren
// 2. Daten für die Einfügung in die Datenbank vorbereiten
// 3. Benutzer in die Datenbank einfügen oder Library API aufrufen
// Aktuelle Schritte:
// 4. Benutzersitzung erstellen
await createSession(user.id)
// 5. Benutzer weiterleiten
redirect('/profile')
}
Tipps:
- Cookies sollten auf dem Server gesetzt werden, um clientseitige Manipulationen zu verhindern.
- 🎥 Video: Erfahren Sie mehr über zustandslose Sitzungen und Authentifizierung mit Next.js → YouTube (11 Minuten).
Aktualisieren (oder erneuern) von Sitzungen
Sie können auch die Ablaufzeit der Sitzung verlängern. Dies ist nützlich, um den Benutzer angemeldet zu halten, nachdem er die Anwendung erneut aufgerufen hat. Zum Beispiel:
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export async function updateSession() {
const session = (await cookies()).get('session')?.value
const payload = await decrypt(session)
if (!session || !payload) {
return null
}
const expires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)(
await cookies()
).set('session', session, {
httpOnly: true,
secure: true,
expires: expires,
sameSite: 'lax',
path: '/',
})
}
Tipp: Prüfen Sie, ob Ihre Auth Library Refresh-Tokens unterstützt, die zur Verlängerung der Benutzersitzung verwendet werden können.
Löschen der Sitzung
Um die Sitzung zu löschen, können Sie das Cookie entfernen:
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}
import 'server-only'
import { cookies } from 'next/headers'
export async function deleteSession() {
const cookieStore = await cookies()
cookieStore.delete('session')
}
Anschließend können Sie die Funktion deleteSession()
in Ihrer Anwendung wiederverwenden, beispielsweise beim Abmelden:
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
await deleteSession()
redirect('/login')
}
import { cookies } from 'next/headers'
import { deleteSession } from '@/app/lib/session'
export async function logout() {
await deleteSession()
redirect('/login')
}
Datenbank-Sitzungen
Um Datenbank-Sitzungen zu erstellen und zu verwalten, müssen Sie folgende Schritte durchführen:
- Erstellen Sie eine Tabelle in Ihrer Datenbank zur Speicherung von Sitzungen und Daten (oder prüfen Sie, ob Ihre Authentifizierungsbibliothek dies übernimmt).
- Implementieren Sie Funktionen zum Einfügen, Aktualisieren und Löschen von Sitzungen
- Verschlüsseln Sie die Sitzungs-ID, bevor Sie sie im Browser des Benutzers speichern, und stellen Sie sicher, dass Datenbank und Cookie synchron bleiben (dies ist optional, wird aber für optimistische Authentifizierungsprüfungen in Middleware empfohlen).
Beispiel:
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id: number) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. Sitzung in der Datenbank erstellen
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Sitzungs-ID zurückgeben
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. Sitzungs-ID verschlüsseln
const session = await encrypt({ sessionId, expiresAt })
// 3. Sitzung in Cookies für optimistische Authentifizierungsprüfungen speichern
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
import cookies from 'next/headers'
import { db } from '@/app/lib/db'
import { encrypt } from '@/app/lib/session'
export async function createSession(id) {
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
// 1. Sitzung in der Datenbank erstellen
const data = await db
.insert(sessions)
.values({
userId: id,
expiresAt,
})
// Sitzungs-ID zurückgeben
.returning({ id: sessions.id })
const sessionId = data[0].id
// 2. Sitzungs-ID verschlüsseln
const session = await encrypt({ sessionId, expiresAt })
// 3. Sitzung in Cookies für optimistische Authentifizierungsprüfungen speichern
const cookieStore = await cookies()
cookieStore.set('session', session, {
httpOnly: true,
secure: true,
expires: expiresAt,
sameSite: 'lax',
path: '/',
})
}
Tipps:
- Für schnelleren Zugriff können Sie Server-Caching für die Lebensdauer der Sitzung in Betracht ziehen. Sie können die Sitzungsdaten auch in Ihrer primären Datenbank belassen und Datenanfragen kombinieren, um die Anzahl der Abfragen zu reduzieren.
- Sie können Datenbank-Sitzungen für erweiterte Anwendungsfälle verwenden, z.B. um den Zeitpunkt der letzten Anmeldung eines Benutzers zu verfolgen, die Anzahl aktiver Geräte zu überwachen oder Benutzern die Möglichkeit zu geben, sich von allen Geräten abzumelden.
Nach der Implementierung der Sitzungsverwaltung müssen Sie Autorisierungslogik hinzufügen, um zu steuern, was Benutzer in Ihrer Anwendung aufrufen und tun können. Fahren Sie mit dem Abschnitt Autorisierung fort, um mehr zu erfahren.
Autorisierung
Sobald ein Benutzer authentifiziert ist und eine Sitzung erstellt wurde, können Sie die Autorisierung implementieren, um zu steuern, was der Benutzer in Ihrer Anwendung aufrufen und tun kann.
Es gibt zwei Haupttypen von Autorisierungsprüfungen:
- Optimistisch: Überprüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen oder eine Aktion auszuführen, indem die im Cookie gespeicherten Sitzungsdaten verwendet werden. Diese Prüfungen sind nützlich für schnelle Operationen, wie das Anzeigen/Ausblenden von UI-Elementen oder das Umleiten von Benutzern basierend auf Berechtigungen oder Rollen.
- Sicher: Überprüft, ob der Benutzer berechtigt ist, auf eine Route zuzugreifen oder eine Aktion auszuführen, indem die in der Datenbank gespeicherten Sitzungsdaten verwendet werden. Diese Prüfungen sind sicherer und werden für Operationen verwendet, die Zugriff auf sensible Daten oder Aktionen erfordern.
Für beide Fälle empfehlen wir:
- Erstellen einer Datenzugriffsschicht (DAL) zur Zentralisierung Ihrer Autorisierungslogik
- Verwenden von Data Transfer Objects (DTO), um nur die notwendigen Daten zurückzugeben
- Optional die Verwendung von Middleware für optimistische Prüfungen.
Optimistische Prüfungen mit Middleware (Optional)
Es gibt Fälle, in denen Sie Middleware verwenden und Benutzer basierend auf Berechtigungen umleiten möchten:
- Zur Durchführung optimistischer Prüfungen. Da Middleware auf jeder Route läuft, ist sie eine gute Möglichkeit, Umleitungslogik zu zentralisieren und nicht autorisierte Benutzer vorzufiltern.
- Zum Schutz statischer Routen, die Daten zwischen Benutzern teilen (z.B. Inhalte hinter einer Paywall).
Da Middleware jedoch auf jeder Route läuft, einschließlich vorab geladener Routen, ist es wichtig, nur die Sitzung aus dem Cookie zu lesen (optimistische Prüfungen) und Datenbankprüfungen zu vermeiden, um Leistungsprobleme zu verhindern.
Beispiel:
import { NextRequest, NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. Geschützte und öffentliche Routen angeben
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req: NextRequest) {
// 2. Prüfen, ob die aktuelle Route geschützt oder öffentlich ist
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. Sitzung aus dem Cookie entschlüsseln
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 4. Zu /login umleiten, wenn der Benutzer nicht authentifiziert ist
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 5. Zu /dashboard umleiten, wenn der Benutzer authentifiziert ist
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// Routen, auf denen Middleware nicht laufen sollte
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
import { NextResponse } from 'next/server'
import { decrypt } from '@/app/lib/session'
import { cookies } from 'next/headers'
// 1. Geschützte und öffentliche Routen angeben
const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/signup', '/']
export default async function middleware(req) {
// 2. Prüfen, ob die aktuelle Route geschützt oder öffentlich ist
const path = req.nextUrl.pathname
const isProtectedRoute = protectedRoutes.includes(path)
const isPublicRoute = publicRoutes.includes(path)
// 3. Sitzung aus dem Cookie entschlüsseln
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
// 5. Zu /login umleiten, wenn der Benutzer nicht authentifiziert ist
if (isProtectedRoute && !session?.userId) {
return NextResponse.redirect(new URL('/login', req.nextUrl))
}
// 6. Zu /dashboard umleiten, wenn der Benutzer authentifiziert ist
if (
isPublicRoute &&
session?.userId &&
!req.nextUrl.pathname.startsWith('/dashboard')
) {
return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
}
return NextResponse.next()
}
// Routen, auf denen Middleware nicht laufen sollte
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
Während Middleware für erste Prüfungen nützlich sein kann, sollte sie nicht Ihre einzige Verteidigungslinie zum Schutz Ihrer Daten sein. Die meisten Sicherheitsprüfungen sollten so nah wie möglich an Ihrer Datenquelle durchgeführt werden, siehe Datenzugriffsschicht für weitere Informationen.
Tipps:
- In Middleware können Sie Cookies auch mit
req.cookies.get('session').value
lesen.- Middleware verwendet die Edge Runtime, prüfen Sie, ob Ihre Authentifizierungs- und Sitzungsverwaltungsbibliothek kompatibel sind.
- Sie können die
matcher
-Eigenschaft in der Middleware verwenden, um anzugeben, auf welchen Routen Middleware laufen soll. Für die Authentifizierung wird jedoch empfohlen, dass Middleware auf allen Routen läuft.
Erstellen einer Datenzugriffsschicht (DAL)
Wir empfehlen die Erstellung einer DAL zur Zentralisierung Ihrer Datenanfragen und Autorisierungslogik.
Die DAL sollte eine Funktion enthalten, die die Sitzung des Benutzers während der Interaktion mit Ihrer Anwendung überprüft. Mindestens sollte die Funktion prüfen, ob die Sitzung gültig ist, und dann den Benutzer umleiten oder die für weitere Anfragen benötigten Benutzerinformationen zurückgeben.
Erstellen Sie beispielsweise eine separate Datei für Ihre DAL, die eine verifySession()
-Funktion enthält. Verwenden Sie dann die cache-API von React, um den Rückgabewert der Funktion während eines React-Renderdurchlaufs zwischenzuspeichern:
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session?.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
import 'server-only'
import { cookies } from 'next/headers'
import { decrypt } from '@/app/lib/session'
export const verifySession = cache(async () => {
const cookie = (await cookies()).get('session')?.value
const session = await decrypt(cookie)
if (!session.userId) {
redirect('/login')
}
return { isAuth: true, userId: session.userId }
})
Sie können dann die Funktion verifySession()
in Ihren Datenanfragen, Server-Aktionen und Route Handlern aufrufen:
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explizit die benötigten Spalten zurückgeben statt des gesamten Benutzerobjekts
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
try {
const data = await db.query.users.findMany({
where: eq(users.id, session.userId),
// Explizit die benötigten Spalten zurückgeben statt des gesamten Benutzerobjekts
columns: {
id: true,
name: true,
email: true,
},
})
const user = data[0]
return user
} catch (error) {
console.log('Failed to fetch user')
return null
}
})
Tipp:
- Eine DAL kann zum Schutz von Daten verwendet werden, die zur Laufzeit abgefragt werden. Für statische Routen, die Daten zwischen Benutzern teilen, werden die Daten jedoch zum Build-Zeitpunkt und nicht zur Laufzeit abgefragt. Verwenden Sie Middleware zum Schutz statischer Routen.
- Für sichere Prüfungen können Sie überprüfen, ob die Sitzung gültig ist, indem Sie die Sitzungs-ID mit Ihrer Datenbank vergleichen. Verwenden Sie die cache-Funktion von React, um unnötige doppelte Anfragen an die Datenbank während eines Renderdurchlaufs zu vermeiden.
- Sie können verwandte Datenanfragen in einer JavaScript-Klasse konsolidieren, die
verifySession()
vor allen Methoden ausführt.
Verwendung von Data Transfer Objects (DTO)
Beim Abrufen von Daten wird empfohlen, nur die notwendigen Daten zurückzugeben, die in Ihrer Anwendung verwendet werden, und nicht ganze Objekte. Zum Beispiel, wenn Sie Benutzerdaten abfragen, könnten Sie nur die Benutzer-ID und den Namen zurückgeben, anstatt das gesamte Benutzerobjekt, das Passwörter, Telefonnummern usw. enthalten könnte.
Falls Sie jedoch keine Kontrolle über die zurückgegebene Datenstruktur haben oder in einem Team arbeiten, wo Sie vermeiden möchten, dass ganze Objekte an den Client übergeben werden, können Sie Strategien wie die Spezifizierung der Felder verwenden, die sicher für den Client freigegeben werden können.
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer: User) {
return true
}
function canSeePhoneNumber(viewer: User, team: string) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug: string) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Hier spezifische Spalten zurückgeben
})
const user = data[0]
const currentUser = await getUser(user.id)
// Oder nur das zurückgeben, was für die Abfrage spezifisch ist
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
import 'server-only'
import { getUser } from '@/app/lib/dal'
function canSeeUsername(viewer) {
return true
}
function canSeePhoneNumber(viewer, team) {
return viewer.isAdmin || team === viewer.team
}
export async function getProfileDTO(slug) {
const data = await db.query.users.findMany({
where: eq(users.slug, slug),
// Hier spezifische Spalten zurückgeben
})
const user = data[0]
const currentUser = await getUser(user.id)
// Oder nur das zurückgeben, was für die Abfrage spezifisch ist
return {
username: canSeeUsername(currentUser) ? user.username : null,
phonenumber: canSeePhoneNumber(currentUser, user.team)
? user.phonenumber
: null,
}
}
Indem Sie Ihre Datenanfragen und Autorisierungslogik in einer DAL (Data Access Layer) zentralisieren und DTOs verwenden, können Sie sicherstellen, dass alle Datenanfragen sicher und konsistent sind, was die Wartung, Überprüfung und Fehlerbehebung bei der Skalierung Ihrer Anwendung erleichtert.
Gut zu wissen:
- Es gibt verschiedene Möglichkeiten, ein DTO zu definieren, von der Verwendung von
toJSON()
über individuelle Funktionen wie im obigen Beispiel bis hin zu JS-Klassen. Da dies JavaScript-Muster und keine React- oder Next.js-Funktionen sind, empfehlen wir, nachzuforschen, um das beste Muster für Ihre Anwendung zu finden.- Erfahren Sie mehr über Sicherheitsbest Practices in unserem Artikel zur Sicherheit in Next.js.
Server-Komponenten
Authentifizierungsprüfungen in Server-Komponenten sind nützlich für rollenbasierten Zugriff. Zum Beispiel, um Komponenten basierend auf der Benutzerrolle bedingt zu rendern:
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session?.user?.role // Annahme: 'role' ist Teil des Session-Objekts
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
import { verifySession } from '@/app/lib/dal'
export default function Dashboard() {
const session = await verifySession()
const userRole = session.role // Annahme: 'role' ist Teil des Session-Objekts
if (userRole === 'admin') {
return <AdminDashboard />
} else if (userRole === 'user') {
return <UserDashboard />
} else {
redirect('/login')
}
}
Im Beispiel verwenden wir die Funktion verifySession()
aus unserer DAL, um nach 'admin', 'user' und nicht autorisierten Rollen zu prüfen. Dieses Muster stellt sicher, dass jeder Benutzer nur mit den für seine Rolle geeigneten Komponenten interagiert.
Layouts und Authentifizierungsprüfungen
Aufgrund des Partiellen Renderings (Partial Rendering) ist Vorsicht geboten, wenn Prüfungen in Layouts durchgeführt werden, da diese bei der Navigation nicht neu gerendert werden, was bedeutet, dass die Benutzersitzung nicht bei jedem Routenwechsel überprüft wird.
Stattdessen sollten Sie die Prüfungen in der Nähe Ihrer Datenquelle oder der Komponente durchführen, die bedingt gerendert wird.
Zum Beispiel, betrachten Sie ein gemeinsames Layout, das die Benutzerdaten abruft und das Benutzerbild in einer Navigation anzeigt. Anstatt die Authentifizierungsprüfung im Layout durchzuführen, sollten Sie die Benutzerdaten (getUser()
) im Layout abrufen und die Authentifizierungsprüfung in Ihrer DAL durchführen.
Dies stellt sicher, dass, wo immer getUser()
in Ihrer Anwendung aufgerufen wird, die Authentifizierungsprüfung durchgeführt wird, und verhindert, dass Entwickler vergessen zu prüfen, ob der Benutzer berechtigt ist, auf die Daten zuzugreifen.
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await getUser();
return (
// ...
)
}
export default async function Layout({ children }) {
const user = await getUser();
return (
// ...
)
}
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Benutzer-ID aus der Session abrufen und Daten abfragen
})
export const getUser = cache(async () => {
const session = await verifySession()
if (!session) return null
// Benutzer-ID aus der Session abrufen und Daten abfragen
})
Gut zu wissen:
- Ein gängiges Muster in SPAs ist,
return null
in einem Layout oder einer Top-Level-Komponente zurückzugeben, wenn ein Benutzer nicht autorisiert ist. Dieses Muster wird nicht empfohlen, da Next.js-Anwendungen mehrere Einstiegspunkte haben, die nicht verhindern, dass verschachtelte Routensegmente und Server-Aktionen aufgerufen werden können.
Server-Aktionen
Behandeln Sie Server-Aktionen mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte und überprüfen Sie, ob der Benutzer berechtigt ist, eine Mutation durchzuführen.
Im folgenden Beispiel prüfen wir die Benutzerrolle, bevor die Aktion fortgesetzt wird:
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction(formData: FormData) {
const session = await verifySession()
const userRole = session?.user?.role
// Frühzeitig zurückkehren, wenn der Benutzer nicht berechtigt ist, die Aktion durchzuführen
if (userRole !== 'admin') {
return null
}
// Für autorisierte Benutzer mit der Aktion fortfahren
}
'use server'
import { verifySession } from '@/app/lib/dal'
export async function serverAction() {
const session = await verifySession()
const userRole = session.user.role
// Frühzeitig zurückkehren, wenn der Benutzer nicht berechtigt ist, die Aktion durchzuführen
if (userRole !== 'admin') {
return null
}
// Für autorisierte Benutzer mit der Aktion fortfahren
}
Route-Handler
Behandeln Sie Route-Handler mit den gleichen Sicherheitsüberlegungen wie öffentlich zugängliche API-Endpunkte und überprüfen Sie, ob der Benutzer berechtigt ist, auf den Route-Handler zuzugreifen.
Zum Beispiel:
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// Benutzerauthentifizierung und Rollenüberprüfung
const session = await verifySession()
// Prüfen, ob der Benutzer authentifiziert ist
if (!session) {
// Benutzer ist nicht authentifiziert
return new Response(null, { status: 401 })
}
// Prüfen, ob der Benutzer die 'admin'-Rolle hat
if (session.user.role !== 'admin') {
// Benutzer ist authentifiziert, hat aber nicht die erforderlichen Berechtigungen
return new Response(null, { status: 403 })
}
// Für autorisierte Benutzer fortfahren
}
import { verifySession } from '@/app/lib/dal'
export async function GET() {
// Benutzerauthentifizierung und Rollenüberprüfung
const session = await verifySession()
// Prüfen, ob der Benutzer authentifiziert ist
if (!session) {
// Benutzer ist nicht authentifiziert
return new Response(null, { status: 401 })
}
// Prüfen, ob der Benutzer die 'admin'-Rolle hat
if (session.user.role !== 'admin') {
// Benutzer ist authentifiziert, hat aber nicht die erforderlichen Berechtigungen
return new Response(null, { status: 403 })
}
// Für autorisierte Benutzer fortfahren
}
Das obige Beispiel zeigt einen Route-Handler mit einer zweistufigen Sicherheitsprüfung. Zuerst wird auf eine aktive Sitzung geprüft, und dann wird verifiziert, ob der angemeldete Benutzer ein 'admin' ist.
Context-Provider
Die Verwendung von Context-Providern für die Authentifizierung funktioniert aufgrund von Interleaving. Allerdings wird React context
in Server-Komponenten nicht unterstützt, wodurch sie nur auf Client-Komponenten anwendbar sind.
Dies funktioniert, aber alle untergeordneten Server-Komponenten werden zuerst auf dem Server gerendert und haben keinen Zugriff auf die Sitzungsdaten des Context-Providers:
import { ContextProvider } from 'auth-lib'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<ContextProvider>{children}</ContextProvider>
</body>
</html>
)
}
'use client';
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
'use client';
import { useSession } from "auth-lib";
export default function Profile() {
const { userId } = useSession();
const { data } = useSWR(`/api/user/${userId}`, fetcher)
return (
// ...
);
}
Falls Sitzungsdaten in Client-Komponenten benötigt werden (z.B. für clientseitiges Datenabrufen), verwenden Sie Reacts taintUniqueValue
API, um zu verhindern, dass sensible Sitzungsdaten dem Client ausgesetzt werden.
Ressourcen
Nachdem Sie nun etwas über Authentifizierung in Next.js gelernt haben, finden Sie hier Next.js-kompatible Bibliotheken und Ressourcen, die Ihnen bei der Implementierung sicherer Authentifizierung und Sitzungsverwaltung helfen:
Authentifizierungsbibliotheken
Sitzungsverwaltungsbibliotheken
Weiterführende Literatur
Um weiter über Authentifizierung und Sicherheit zu lernen, lesen Sie die folgenden Ressourcen: