So fügen Sie einer Astro-Site Authentifizierung hinzu
Richte Astro-Authentifizierung mit Adapter, Middleware, Astro.locals, Sessions, rewrite und Actions ein, inkl. Login, Logout und Routenschutz.
Authentifizierung in Astro hängt ebenso sehr davon ab, wie eine Seite gerendert wird, wie von der gewählten Bibliothek. Standardmäßig prerendert Astro die Seiten Ihres Projekts zur Build-Zeit, und Sie nehmen einzelne Routen gezielt davon aus. Eine prerenderte Seite wird erzeugt, bevor überhaupt ein Besucher existiert – es gibt also keinen Request, keinen Cookie-Header und keine Session, die man auslesen könnte.
Ein erster Versuch scheitert aus diesem Grund häufig lautlos. Sie installieren eine Auth-Bibliothek, fügen deren Middleware in src/middleware.ts ein, rufen /account auf – und Astro.locals ist ein leeres Objekt. Kein Fehler, kein Log, kein Stacktrace, nach dem man suchen könnte.
Dieser Artikel geht die gesamte Kette der Reihe nach durch, Datei für Datei: den Adapter, den routenspezifischen prerender-Export, die Middleware, die den Benutzer auflöst, die Seite, die ihn ausliest, Routenschutz mit rewrite(), Login und Logout als Astro Actions sowie die Grenze, ab der clientseitige Islands nichts davon mehr sehen. Der Code zielt auf Astro 7.x ab. Astro 6 hat die Node-Mindestversion auf 22 angehoben und die Unterstützung für Node 18 und 20 eingestellt – verwenden Sie also Node 22.12.0 oder neuer.
Die wichtigsten Erkenntnisse
- Astro prerendert standardmäßig das gesamte Projekt. Jede Seite, die eine Session ausliest, muss sich deshalb mit
export const prerender = falsedavon ausnehmen, und On-Demand-Rendering setzt einen Adapter voraus. - Wenn Ihre Middleware scheinbar nichts tut, ist die Route, die sie schützen soll, mit ziemlicher Sicherheit noch prerendert.
Astro.localsexistiert exakt für die Dauer eines Routen-Renderings; Daten, die den nächsten Request überdauern müssen, gehören inAstro.session, was einen Session-Treiber erfordert.- Astro Actions sind öffentlich erreichbare HTTP-Endpunkte. Jeder Handler braucht daher seine eigene
context.locals-Prüfung; die Seite zu schützen, die das Formular rendert, schützt nicht die dahinterliegende Action. Astro.localsundAstro.sessionexistieren ausschließlich serverseitig. Eine Komponente mit einerclient:*-Direktive sieht nur das, was die Seite ihr als Props übergeben hat.
Warum bricht „static by default” die Astro-Authentifizierung?
Ein Astro-Projekt wird zu statischem HTML gebaut, sofern eine Route nichts anderes anfordert – und statisches HTML wird einmalig zur Build-Zeit erzeugt, für alle Besucher gleich. Das Prerendern des gesamten Projekts ist der dokumentierte Standard, wie der Guide zum On-Demand-Rendering darlegt. Eine Session lebt in einem Request-Header, der noch nicht existiert, wenn dieses HTML auf die Festplatte geschrieben wird. Cookie-Zugriffe, Astro.request und alles, was eine Middleware in locals ablegt, haben somit keinen Anknüpfungspunkt.
Die praktische Konsequenz: Middleware, die scheinbar nie ausgeführt wird, ist ein Symptom des Rendering-Modus und kein Bug der Auth-Bibliothek.
Wie aktiviert man On-Demand-Rendering?
On-Demand-Rendering braucht zwei Dinge: einen Adapter, der einen Server für Ihre Ziel-Runtime erzeugt, und ein Opt-out pro Route. Astros First-Party-Adapter sind @astrojs/cloudflare, @astrojs/netlify, @astrojs/node und @astrojs/vercel, daneben existieren Community-Adapter. Die Option adapter ist in der Konfigurationsreferenz dokumentiert. Installieren Sie einen mit npx astro add node und prüfen Sie die jeweilige Adapter-Seite, da sich die Konfigurationsoptionen unterscheiden.
Anschließend wählen Sie eine Grundform. Die Option output kennt genau zwei Werte: 'static' und 'server'.
| Art der Site | astro.config.mjs | Page-Frontmatter |
|---|---|---|
| Content-Site mit wenigen eingeloggten Seiten | Adapter installiert, Standard output: 'static' beibehalten | export const prerender = false auf jeder Route, die eine Session liest |
| Überwiegend eingeloggte App | Adapter installiert, output: 'server' setzen | export const prerender = true auf Marketing- und Content-Routen |
Was auch immer Sie wählen – die Regel bleibt dieselbe: Die Route, die die Session liest, muss on demand gerendert werden. Die Dokumentation zum Node-Adapter behandelt die Runtime-Spezifika für das hier gezeigte Beispiel.
Die Middleware: src/middleware.ts
Die Middleware löst den Benutzer einmal pro Request auf und reicht ihn an alles Nachgelagerte weiter. Legen Sie die Datei unter src/middleware.js|ts ab – oder unter src/middleware/index.js|ts, wenn Sie einen Ordner bevorzugen – und versehen Sie sie mit einem benannten Export namens onRequest. Ein Default-Export wird nicht erkannt; darauf weist der Middleware-Guide ausdrücklich hin.
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware(async (context, next) => {
const stored = (await context.session?.get('user')) ?? null;
context.locals.user = stored as User | null;
return next();
});
Hier wird Astros eingebaute Session gelesen, statt ein Cookie von Hand zu parsen. Dieselbe Session taucht unter zwei Namen auf, wie Astros Sessions-Guide beschreibt: Seiten und Komponenten greifen über Astro.session darauf zu, während Middleware, API-Endpunkte und Action-Handler sie aus context.session beziehen. Die Speicherung erfolgt nicht automatisch. Drei Adapter wählen einen Standard-Treiber für Sie aus – Node, Cloudflare und Netlify; bei jedem anderen Adapter benennen Sie ihn selbst, gemäß der Session-Treiber-Referenz. Eine Einschränkung sollten Sie kennen, bevor Sie in eine Edge-Runtime deployen: Sessions funktionieren nicht in Edge-Middleware.
Typisieren Sie locals, indem Sie den App-Namespace in src/env.d.ts erweitern:
// src/env.d.ts
type User = {
id: string;
email: string;
};
declare namespace App {
interface Locals {
user: User | null;
}
}
Den Benutzer mit Astro.locals auslesen
Eine Seite liest genau das, was die Middleware zugewiesen hat – vorausgesetzt, diese Seite wird on demand gerendert. Astro.locals existiert exakt für die Dauer eines Routen-Renderings. Es ist damit der richtige Ort, um ein Benutzerobjekt von der Middleware zur Seite zu transportieren, und der falsche Ort für alles, was den nächsten Request überdauern muss.
---
// src/pages/account.astro
/* On-demand rendering */ export const prerender = false;
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login');
---
<h1>Signed in as {user.email}</h1>
Löschen Sie die erste Zeile, und die Seite wandert zurück in die Prerender-Menge: Sie wird zu statischem HTML gebaut, Astro.locals.user wird nie befüllt, und jeder Besucher erhält dieselbe Datei. Diese eine Zeile ist der Unterschied zwischen funktionierender Authentifizierung und einem Login-Screen, an dem niemand vorbeikommt.
Wie schützt man Routen mit context.rewrite()?
Schützen Sie Routen, indem Sie die Zugangskontrolle in der Middleware zentralisieren und die Login-Seite an Ort und Stelle rendern, statt dorthin weiterzuleiten. context.rewrite('/login') zeigt anderen Inhalt unter der vom Besucher angefragten URL an, wodurch der geschützte Pfad in der Adressleiste stehen bleibt.
// src/middleware.ts
import { defineMiddleware } from 'astro:middleware';
const protectedPaths = ['/account', '/dashboard'];
export const onRequest = defineMiddleware(async (context, next) => {
const stored = (await context.session?.get('user')) ?? null;
context.locals.user = stored as User | null;
const needsAuth = protectedPaths.some((path) =>
context.url.pathname.startsWith(path),
);
if (needsAuth && !context.locals.user) {
return context.rewrite('/login');
}
return next();
});
Da der Rewrite das Rendering neu startet, läuft Ihre Middleware dabei ein zweites Mal. Halten Sie /login deshalb aus protectedPaths heraus, sonst scheitert dieser zweite Durchlauf an derselben Prüfung und es entsteht eine Schleife. Defekte Authentifizierung dieser Art wirft selten eine Exception: Ein Session Replay eines Login-Flows zeigt, wie der Benutzer zwischen Login-Seite und geschützter Route hin- und herspringt – genau so sieht ein falsch gescoptes Session-Cookie oder eine im falschen Zweig platzierte Zugangskontrolle von außen aus.
Vermeiden Sie hier die andere Rewrite-Variante. Wenn Sie next() ein Request-Objekt übergeben, baut Astro aus dem alten Request einen Ersatz-Request, und jeder Versuch, den Body danach (oder davor) zu lesen, wirft zur Laufzeit einen Fehler. Am schmerzhaftesten wird das, wenn eine Action von einem HTML-Formular angetrieben wird – deshalb verweist die Dokumentation stattdessen auf context.rewrite() bzw. Astro.rewrite().
Login und Logout mit Astro Actions
Astro Actions, eingeführt in astro@4.15, sind der eingebaute Weg, Login und Logout zu handhaben, und sie ersetzen selbstgebaute API-Routen. Definieren Sie sie in einem server-Objekt, das aus src/actions/index.ts exportiert wird, setzen Sie accept: 'form' und senden Sie aus reinem HTML an sie.
// src/actions/index.ts
import { ActionError, defineAction } from 'astro:actions';
import { z } from 'astro/zod';
// Replace with your own credential lookup.
async function verifyCredentials(email: string, password: string): Promise<User | null> {
return null;
}
export const server = {
login: defineAction({
accept: 'form',
input: z.object({
email: z.email({ error: 'Enter a valid email address.' }),
password: z.string(),
}),
handler: async ({ email, password }, context) => {
const user = await verifyCredentials(email, password);
if (!user) throw new ActionError({ code: 'UNAUTHORIZED' });
await context.session?.regenerate();
await context.session?.set('user', user);
return { ok: true };
},
}),
logout: defineAction({
accept: 'form',
handler: async (_input, context) => {
await context.session?.destroy();
return { ok: true };
},
}),
deleteAccount: defineAction({
accept: 'form',
handler: async (_input, context) => {
if (!context.locals.user) throw new ActionError({ code: 'UNAUTHORIZED' });
return { ok: true };
},
}),
};
z stammt aus astro/zod, das Zod v4 re-exportiert. Top-Level-Validatoren wie z.email() und der error-Key für eigene Fehlermeldungen sind damit die aktuelle Schreibweise. Das Neugenerieren der Session-ID beim Login schützt vor Session Fixation, und destroy() löscht das Cookie und verwirft die gespeicherten Daten auf dem Server.
Beachten Sie deleteAccount. Actions sind öffentlich zugängliche Endpunkte mit eigenen URLs – jeder kann eine direkt aufrufen, ohne jemals die Seite zu laden, die das zugehörige Formular rendert. Jeder Handler, der Benutzerdaten anfasst, prüft context.locals selbst.
Auf Seiten der Page braucht es ein Formular und das Auslesen des Ergebnisses:
---
// src/pages/login.astro
/* On-demand rendering */ export const prerender = false;
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.login);
if (result && !result.error) return Astro.redirect('/account');
---
{result?.error && <p class="error">Those details did not match.</p>}
<form method="POST" action={actions.login}>
<input type="email" name="email" required />
<input type="password" name="password" required />
<button>Log in</button>
</form>
Logout hat dieselbe Form: <form method="POST" action={actions.logout}>, ganz ohne clientseitiges JavaScript.
Die Island-Falle: Islands sehen locals nie
Astro.locals und Astro.session existieren ausschließlich serverseitig. Middleware, .astro-Seiten und Layouts, API-Routen und Action-Handler laufen allesamt auf dem Server und teilen sie sich. Eine Komponente mit einer client:*-Direktive hydratisiert im Browser, steht außerhalb dieser Kette und sieht nur das, was die Seite ihr als Props übergeben hat.
---
// Renders logged-out UI forever. The island cannot reach locals.
import UserMenu from '../components/UserMenu.jsx';
---
<UserMenu client:load />
---
// Correct: the page reads locals on the server and passes the value down.
import UserMenu from '../components/UserMenu.jsx';
const user = Astro.locals.user;
---
<UserMenu client:load user={user} />
In der fehlerhaften Variante wird keine Exception geworfen. Die Seite rendert eingeloggte Inhalte, während die Island daneben einen Anmelde-Button rendert – eine Diskrepanz, die sich ausschließlich visuell zeigt.
Dieses Beispiel nutzt Astros eingebaute Sessions, und dieselbe Abfolge gilt auch für eine Bibliothek: Astros Authentifizierungs-Guide verweist auf Auth-Bibliotheken wie Better Auth und Clerk für E-Mail-Anmeldung und OAuth, und Better Auths framework-agnostisches Design erstreckt sich auch auf Astro, wie unsere BetterAuth-Übersicht erläutert. Was Sie auch wählen: Es braucht weiterhin einen Adapter, eine nicht prerenderte Route und eine Middleware, die in context.locals schreibt.
Fazit
Authentifizierung in Astro ist eine Kette mit einem schwachen Glied ganz vorn: kein Adapter und kein prerender = false bedeutet keinen Request – und alles Nachgelagerte tut stillschweigend nichts. Bringen Sie zuerst den Rendering-Modus in Ordnung, dann die Middleware, dann die Prüfungen pro Handler in Ihren Actions. Öffnen Sie den Guide zum On-Demand-Rendering neben Ihrer astro.config.mjs, installieren Sie einen Adapter und fügen Sie export const prerender = false zur ersten Seite hinzu, die einen Benutzer benötigt.
FAQs
Funktionieren Astro Actions auf einer prerenderten Seite?
Nein. Eine Seite muss on demand gerendert werden, um eine Action über eine Formular-Action aufrufen zu können. Fügen Sie also 'export const prerender = false' zu der Seite hinzu, die das Formular enthält, und installieren Sie einen Adapter, damit ein Server existiert, der den Handler ausführt. Request-Bodies von Actions haben außerdem eine Standardobergrenze von 1 MB (1048576 Bytes); erhöhen Sie security.actionBodySizeLimit, wenn ein Handler etwas Größeres entgegennehmen muss, etwa einen Upload.
Kann eine prerenderte statische Seite eingeloggte oder ausgeloggte UI anzeigen?
Nicht auf dem Server. Eine prerenderte Seite wird zur Build-Zeit auf die Festplatte geschrieben, und jeder Besucher erhält dieselbe Datei – es gibt also keinen Cookie-Header, anhand dessen verzweigt werden könnte. Zwei Lösungen funktionieren: Nehmen Sie diese Route mit 'export const prerender = false' vom Prerendering aus, oder belassen Sie die Seite statisch und laden Sie den Benutzer im Browser von einem On-Demand-Endpunkt, um das Ergebnis an eine Client-Komponente zu übergeben.
Schützt Astro Login-Formulare automatisch gegen CSRF?
Teilweise. Auf on demand gerenderten Seiten vergleicht Astro den vom Browser gesendeten Origin-Header mit der URL, an die der Request ging, und antwortet mit einem 403, wenn beide nicht übereinstimmen. Dieses Verhalten ist seit Astro 5 über die Option security.checkOrigin standardmäßig aktiviert und deckt ausschließlich seitenübergreifende Formularübermittlungen ab. Sie generieren die Session-ID beim Login weiterhin neu und autorisieren weiterhin jeden Action-Handler und jede API-Route einzeln. Wird security.checkOrigin auf false gesetzt, ist die Prüfung deaktiviert.
Sollte ich context.rewrite oder Astro.redirect verwenden, um ausgeloggte Benutzer zur Login-Seite zu schicken?
Verwenden Sie context.rewrite in der Middleware, wenn der Login-Inhalt unter der vom Besucher angefragten URL ausgeliefert werden soll – so bleibt der geschützte Pfad in der Adressleiste stehen und ein zweiter Browser-Roundtrip entfällt. Astro.redirect gibt eine Redirect-Response zurück, und der Browser navigiert zu /login. Ein Rewrite stößt ein frisches Rendering an, wodurch Ihre Middleware erneut läuft – schließen Sie /login deshalb aus Ihren geschützten Pfaden aus, sonst scheitert dieselbe Prüfung immer wieder.
Gain Debugging Superpowers
Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.
Star on GitHub12k