
Rollen & Rechte-Management mit Laravel Spatie integrieren

Wenn du ein professionelles Headless CMS baust, kommst du an Laravel Spatie Rollen nicht vorbei. In Teil 4 unserer Masterclass haben wir mit Laravel Sanctum den Endgegner der Authentifizierung besiegt. Unser System weiß jetzt absolut sicher, wer vor dem Bildschirm sitzt. Doch in einer echten Enterprise-Applikation reicht diese reine Identitätsprüfung bei Weitem nicht aus. Um die Zugriffsrechte perfekt zu steuern und zu verhindern, dass ein normaler Redakteur versehentlich das gesamte System löscht, benötigen wir ein wasserdichtes System für Laravel Spatie Rollen und granulare Berechtigungen.
Stell dir vor, du stellst einen neuen Praktikanten ein. Er loggt sich ein – die Authentifizierung ist erfolgreich. Da er nun ein gültiges Session-Cookie besitzt, sendet er testweise einen DELETE-Request an /api/categories/1 und löscht deine wichtigste Hauptkategorie. Das Frontend geht offline. Ein absoluter Albtraum. Wir müssen zwingend die Brücke zwischen Authentifizierung (Wer bist du?) und Autorisierung (Was darfst du tun?) schlagen.
1. Das Konzept: Warum Spatie der absolute Goldstandard ist
Man könnte Autorisierung theoretisch selbst bauen, indem man einfach eine is_admin Spalte in die users Tabelle der Datenbank einfügt. Für ein kleines Blog-Projekt mag das reichen. Aber was passiert, wenn du plötzlich einen "SEO-Manager" brauchst, der zwar Artikel bearbeiten, aber nicht löschen darf? Was ist mit einem "Gast-Autor", der nur seine eigenen Artikel sehen darf? Dein Controller-Code würde in einem Chaos aus endlosen if ($user->role === 'admin' || $user->role === 'editor') Abfragen ertrinken.
Hier kommt das legendäre Paket spatie/laravel-permission ins Spiel. Wenn es um Laravel Spatie Rollen geht, ist dies mit fast 15.000 GitHub-Stars das absolute Rückgrat der PHP-Community. Es erlaubt uns, feingranulare Rechte (Permissions wie "edit articles", "delete articles") zu erstellen und diese extrem flexibel an übergeordnete Rollen ("Admin", "Editor") oder sogar direkt an einzelne Nutzer zu vergeben.
Wir bauen unsere API-Endpoints damit so um, dass sie jeden illegalen Zugriffsversuch in Millisekunden mit einem sicheren 403 Forbidden HTTP-Statuscode abblocken.
2. Die Festung aufrüsten: Spatie installieren und konfigurieren
Öffne dein Terminal in unserem Backend-Projekt (cms-backend) und installiere das Paket über Composer. Es ist vollständig kompatibel mit der neuesten Laravel 12 Architektur:
composer require spatie/laravel-permissionDas Paket bringt seine eigene, hochintelligente Datenbankstruktur mit, in der später unsere Laravel Spatie Rollen und Rechte gespeichert werden (die sogenannten Pivot-Tabellen). Damit Laravel diese Tabellen anlegen kann, müssen wir die Migrations-Dateien aus dem Paket in unseren eigenen database/migrations Ordner kopieren.
Führe diesen Befehl aus, um die Spatie-Migrationen zu veröffentlichen:
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"Du wirst sehen, dass Laravel eine neue Migrations-Datei generiert hat. Außerdem wurde eine config/permission.php angelegt, in der du tiefgreifende Cache- und Modell-Einstellungen vornehmen kannst. Für unser Setup sind die Standardwerte aber bereits perfekt optimiert.
Lass uns diese neuen Tabellen direkt in unsere Datenbank feuern:
php artisan migrateDas User-Modell mit Spatie-Kräften ausstatten
Damit unser User-Modell jetzt die magischen Fähigkeiten erlernt (wie z. B. die bequeme Code-Abfrage $user->hasRole('Admin')), müssen wir ihm ein neues "Werkzeug" an die Hand geben.
Öffne die Datei app/Models/User.php und binde den HasRoles Trait ein:
1<?php
2
3namespace App\Models;
4
5use Illuminate\Database\Eloquent\Factories\HasFactory;
6use Illuminate\Foundation\Auth\User as Authenticatable;
7use Illuminate\Notifications\Notifiable;
8use Laravel\Sanctum\HasApiTokens;
9// 1. Den Spatie Trait importieren
10use Spatie\Permission\Traits\HasRoles;
11
12class User extends Authenticatable
13{
14 // 2. HasRoles zu den verwendeten Traits hinzufügen
15 use HasApiTokens, HasFactory, Notifiable, HasRoles;
16
17 protected $fillable = [
18 'name',
19 'email',
20 'password',
21 ];
22
23 protected $hidden = [
24 'password',
25 'remember_token',
26 ];
27
28 protected function casts(): array
29 {
30 return [
31 'email_verified_at' => 'datetime',
32 'password' => 'hashed',
33 ];
34 }
35}Das architektonische Fundament ist gegossen! Unsere Benutzer sind nun bereit, offizielle Rollen und Rechte zu empfangen.

3. Automatisierung pur: Ein Seeder für unsere Laravel Spatie Rollen
Wir könnten unsere Rollen nun theoretisch mühsam von Hand über die Laravel Tinker-Konsole in die Datenbank eintragen. Aber wir bauen hier ein Enterprise-System. Wenn du dein Projekt morgen auf einen Staging-Server hochlädst, oder ein neuer Entwickler in dein Team kommt, müssen die exakt gleichen Zugriffsrechte mit einem einzigen Befehl reproduzierbar sein.
Wir lösen das elegant über einen sogenannten Database Seeder. Dieser fungiert als unsere digitale Fabrik, die unsere Laravel Spatie Rollen und die dazugehörigen, feingranularen Rechte vollautomatisch generiert und sofort intelligent miteinander verknüpft.
Öffne dein Backend-Terminal und generiere den neuen Seeder:
php artisan make:seeder RoleAndPermissionSeederÖffne nun die frisch erstellte Datei unter database/seeders/RoleAndPermissionSeeder.php. Hier definieren wir unsere absolute Sicherheitshierarchie. Kopiere diesen Code exakt so hinein:
1<?php
2
3namespace Database\Seeders;
4
5use Illuminate\Database\Seeder;
6use Spatie\Permission\Models\Role;
7use Spatie\Permission\Models\Permission;
8use App\Models\User;
9
10class RoleAndPermissionSeeder extends Seeder
11{
12 public function run(): void
13 {
14 // 1. WICHTIG: Den Cache von Spatie leeren!
15 // Das erspart dir stundenlanges Debugging, falls du den Seeder mehrfach ausführst.
16 app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
17
18 // 2. Feingranulare Rechte (Permissions) in der Datenbank erschaffen
19 $permissions = [
20 'view posts',
21 'create posts',
22 'edit posts',
23 'delete posts',
24 'publish posts',
25 'manage categories'
26 ];
27
28 foreach ($permissions as $permission) {
29 Permission::create(['name' => $permission]);
30 }
31
32 // 3. Laravel Spatie Rollen anlegen und Rechte strategisch zuweisen
33
34 // Der 'Editor' darf Beiträge ansehen, erstellen und bearbeiten.
35 // Er darf sie aber NICHT löschen oder final publizieren.
36 $editorRole = Role::create(['name' => 'Editor']);
37 $editorRole->givePermissionTo([
38 'view posts',
39 'create posts',
40 'edit posts'
41 ]);
42
43 // Der 'Admin' ist der Gott des Systems und bekommt pauschal ALLES
44 $adminRole = Role::create(['name' => 'Admin']);
45 $adminRole->givePermissionTo(Permission::all());
46
47 // 4. Unserem ersten Test-Nutzer (den wir in Teil 2 erstellt haben)
48 // direkt die Admin-Rolle verpassen.
49 $user = User::find(1);
50 if ($user) {
51 // Die magische Zuweisung in Aktion!
52 $user->assignRole('Admin');
53 }
54 }
55}Schau dir die erste Zeile in der run() Methode ganz genau an: forgetCachedPermissions(). Das Paket cacht Berechtigungen extrem aggressiv im Arbeitsspeicher (RAM), um die Datenbankabfragen für jeden Request auf ein Minimum zu reduzieren. Wenn wir diesen Cache beim Neuschreiben der Datenbank nicht leeren, drohen fatale "Geister-Fehler".
Zudem siehst du hier das architektonische Prinzip des "Least Privilege" (Prinzip der geringsten Rechte). Ein Editor bekommt nicht einfach einen pauschalen Freifahrtschein für das CMS, sondern exakt nur die drei Rechte (view, create, edit), die er für seinen täglichen Job zwingend benötigt. Die komplexe Verknüpfung dieser Daten in den Pivot-Tabellen übernimmt das Paket im Hintergrund völlig unsichtbar für uns.
Lass uns die Fabrik anwerfen und unsere Datenbank befüllen. Wenn du dein Projekt komplett frisch aufsetzt, kannst du später auch php artisan migrate:fresh --seed nutzen (wenn der Seeder in der DatabaseSeeder.php registriert ist), aber für uns reicht jetzt der direkte Aufruf dieses einen Seeders:
php artisan db:seed --class=RoleAndPermissionSeederBoom! Wenn du jetzt in dein Datenbank-Tool (wie DBngin, phpMyAdmin oder TablePlus) schaust, wirst du sehen, dass die Tabellen roles, permissions und model_has_roles perfekt mit unseren Daten gefüllt sind. Dein erster User (mit der ID 1) ist nun offiziell ein Administrator.

4. Die API abriegeln: Rechte in Controllern und Form Requests prüfen
Unsere digitale Fabrik hat die Laravel Spatie Rollen und Rechte erfolgreich in der Datenbank verankert. Unser Admin hat pauschal alle Rechte, unser Editor darf nur lesen, erstellen und bearbeiten. Doch aktuell weiß unsere REST API noch absolut nichts von diesen Regeln. Wenn der Editor einen DELETE-Request an /api/posts/5 sendet, wird der Artikel immer noch gelöscht. Das ändern wir jetzt.
Es gibt in Laravel drei elegante Wege, um Spatie-Rechte abzufragen: Über die Routen-Middleware, direkt im Controller oder in den Form Requests. Für eine Enterprise-Architektur nutzen wir eine Kombination aus Middleware (für den groben Zugriff) und Form Requests (für die feingranulare Kontrolle).
Spatie Middleware in Laravel 12 registrieren
Das Spatie-Paket liefert geniale Middleware-Klassen mit, die Anfragen automatisch mit einem 403 Forbidden JSON-Fehler abblocken, wenn der Nutzer nicht die passende Rolle hat. In Laravel 12 müssen wir diese Middleware-Aliase in unserer bootstrap/app.php registrieren, damit wir sie in den Routen nutzen können.
Öffne die bootstrap/app.php und passe den Middleware-Block an:
1<?php
2
3use Illuminate\Foundation\Application;
4use Illuminate\Foundation\Configuration\Exceptions;
5use Illuminate\Foundation\Configuration\Middleware;
6
7return Application::configure(basePath: dirname(__DIR__))
8 ->withRouting(
9 web: __DIR__.'/../routes/web.php',
10 api: __DIR__.'/../routes/api.php',
11 commands: __DIR__.'/../routes/console.php',
12 health: '/up',
13 )
14 ->withMiddleware(function (Middleware $middleware) {
15 $middleware->statefulApi();
16
17 // 1. Spatie Middleware-Aliase für unsere API registrieren
18 $middleware->alias([
19 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
20 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
21 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
22 ]);
23 })
24 ->withExceptions(function (Exceptions $exceptions) {
25 //
26 })->create();Die Routen absichern
Jetzt können wir unsere routes/api.php wie einen Hochsicherheitstrakt verriegeln. Wir hängen die Middleware einfach an unsere Endpunkte.
1use App\Http\Controllers\Api\PostController;
2
3Route::middleware('auth:sanctum')->group(function () {
4
5 // Nur Nutzer mit dem Recht "view posts" dürfen die Liste abrufen
6 Route::get('/posts', [PostController::class, 'index'])
7 ->middleware('permission:view posts');
8
9 // Das Löschen ist streng limitiert auf das Recht "delete posts"
10 Route::delete('/posts/{post}', [PostController::class, 'destroy'])
11 ->middleware('permission:delete posts');
12
13});Wenn jetzt unser Editor (der das Recht delete posts nicht besitzt) versucht, die Lösch-Route aufzurufen, wird der Controller-Code nicht einmal ausgeführt. Die Spatie-Middleware fängt den Request ab und wirft dem Frontend sofort eine saubere JSON-Antwort mit dem Statuscode 403 entgegen. Das ist Effizienz in Perfektion.
Feinschliff in den Form Requests
Oft reicht die Middleware-Ebene allein nicht aus. Stell dir vor, du möchtest, dass ein Editor zwar Artikel bearbeiten (edit posts), aber den Status nicht auf "veröffentlicht" (publish posts) setzen darf. Diese Logik prüfen wir idealerweise direkt in unseren Form Requests, wo auch die Daten validiert werden.
Öffne den UpdatePostRequest (den wir in Teil 3 erstellt haben) unter app/Http/Requests/UpdatePostRequest.php:
1<?php
2
3namespace App\Http\Requests;
4
5use Illuminate\Foundation\Http\FormRequest;
6
7class UpdatePostRequest extends FormRequest
8{
9 /**
10 * Determine if the user is authorized to make this request.
11 */
12 public function authorize(): bool
13 {
14 $user = $this->user();
15
16 // 1. Der User MUSS generell das Recht haben, Artikel zu bearbeiten
17 if (!$user->can('edit posts')) {
18 return false;
19 }
20
21 // 2. Will der User den Artikel veröffentlichen?
22 // Dann prüfen wir explizit das "publish posts" Recht!
23 if ($this->has('is_published') && $this->input('is_published') == true) {
24 return $user->can('publish posts');
25 }
26
27 return true;
28 }
29
30 public function rules(): array
31 {
32 return [
33 'title' => ['sometimes', 'required', 'string', 'max:255'],
34 'content' => ['nullable', 'string'],
35 'is_published' => ['sometimes', 'boolean'],
36 ];
37 }
38}Das ist der Moment, in dem die wahren Stärken der Laravel Spatie Rollen Architektur aufblitzen. Über die genial einfache Methode $user->can('publish posts') (die uns der HasRoles Trait spendiert hat) prüfen wir die Berechtigungen direkt im Herzen unserer Validierungslogik. Versucht der Editor, das Feld is_published: true mitzusenden, schlägt die authorize() Methode fehl. Die API bleibt kugelsicher.

5. Das Frontend intelligent machen: Rechte über die API ausliefern
Unsere Backend-Festung ist nun absolut kugelsicher. Jeder API-Endpunkt prüft strikt die Laravel Spatie Rollen und die damit verbundenen, feingranularen Berechtigungen, bevor er auch nur eine einzige Zeile an die Datenbank schickt. Doch aus Sicht der User Experience (UX) haben wir noch ein Problem.
Wenn unser Editor sich in das Next.js-Frontend einloggt, sieht er aktuell immer noch den roten "Löschen"-Button neben jedem Artikel. Wenn er darauf klickt, feuert das Frontend den Request ab, Laravel blockt ihn ab und wirft einen 403 Forbidden Fehler zurück. Das ist frustrierend für den Nutzer. Ein perfektes Headless CMS ist reaktiv: Es zeigt dem Nutzer von vornherein nur die Werkzeuge an, die er auch tatsächlich benutzen darf.
Damit unser Next.js-Client diese Entscheidungen treffen kann, müssen wir ihm beim Login (oder beim Abruf der Nutzerdaten) genau mitteilen, welche Rechte der aktuelle User besitzt.
Die UserResource anpassen
Erinnerst du dich an unsere API-Resources aus Teil 3? Genau hier greifen wir jetzt ein. Öffne dein Laravel-Backend und erstelle (falls noch nicht vorhanden) eine UserResource:
php artisan make:resource UserResourceÖffne die Datei app/Http/Resources/UserResource.php und erweitere sie um die magischen Spatie-Funktionen:
1<?php
2
3namespace App\Http\Resources;
4
5use Illuminate\Http\Request;
6use Illuminate\Http\Resources\Json\JsonResource;
7
8class UserResource extends JsonResource
9{
10 public function toArray(Request $request): array
11 {
12 return [
13 'id' => $this->id,
14 'name' => $this->name,
15 'email' => $this->email,
16
17 // 1. Wir übergeben die Namen aller Rollen als einfaches Array
18 'roles' => $this->getRoleNames(),
19
20 // 2. WICHTIG: Wir übergeben alle feingranularen Berechtigungen!
21 // Egal ob sie direkt oder über eine Rolle zugewiesen wurden.
22 'permissions' => $this->getAllPermissions()->pluck('name'),
23
24 'created_at' => $this->created_at->toIso8601String(),
25 ];
26 }
27}Wenn wir nun unseren /api/user Endpunkt (den wir in Teil 4 im AuthController gebaut haben) aufrufen, erhält unser Next.js Frontend ein wunderschönes, aufgeräumtes JSON-Paket, das exakt auflistet, was der Nutzer darf:
1{
2 "id": 1,
3 "name": "Max Redakteur",
4 "email": "max@headless.dev",
5 "roles": ["Editor"],
6 "permissions": [
7 "view posts",
8 "create posts",
9 "edit posts"
10 ]
11}Die bedingte Anzeige in Next.js
Jetzt wechseln wir in unser Frontend-Projekt. Da wir die Rechte nun als Array im Nutzerobjekt haben, können wir in unseren React-Komponenten winzige Helfer-Funktionen nutzen, um UI-Elemente dynamisch ein- oder auszublenden.
Hier ist ein Praxisbeispiel, wie das in unserer Datentabelle (aus Teil 5) aussehen könnte:
1// Auszug aus unserer Next.js Client Component
2
3export default function PostTableRow({ post, user }) {
4
5 // Eine kleine Helfer-Funktion, um Rechte zu prüfen
6 const can = (permission: string) => user?.permissions?.includes(permission);
7
8 return (
9 <div className="flex items-center gap-4">
10 {/* Jeder sieht den Ansehen-Button */}
11 <button className="text-blue-400">Ansehen</button>
12
13 {/* Nur Nutzer mit dem "edit posts" Recht sehen diesen Button */}
14 {can('edit posts') && (
15 <button className="text-emerald-400">Bearbeiten</button>
16 )}
17
18 {/* Nur Nutzer mit dem "delete posts" Recht sehen den Löschen-Button */}
19 {can('delete posts') && (
20 <button className="text-red-500 hover:text-red-400 font-bold">
21 Dauerhaft Löschen
22 </button>
23 )}
24 </div>
25 );
26}Das ist Enterprise-Architektur: Das Backend bleibt die absolute "Single Source of Truth" (Zero Trust). Selbst wenn ein technikaffiner Nutzer das Frontend manipuliert und den "Löschen"-Button per Browser-Konsole wieder einblendet, wird der anschließende Request von unserer Spatie-Middleware im Laravel-Controller gnadenlos zerschmettert. Das Frontend fungiert lediglich als höflicher Reiseführer, der verschlossene Türen direkt ausblendet.

Zusammenfassung: Absolute Kontrolle durch Laravel Spatie
Mit der Implementierung von Laravel Spatie Rollen und Berechtigungen haben wir unser Headless CMS von einem unsicheren Prototypen in eine mandantenfähige Enterprise-Lösung verwandelt.
Du hast gelernt, warum eine simple is_admin-Spalte in modernen Applikationen nicht mehr ausreicht und wie Pivot-Tabellen uns feingranulare Rechteverwaltung ermöglichen. Wir haben eine "digitale Fabrik" (Seeder) gebaut, um unsere Rollen reproduzierbar und automatisiert zu generieren. Durch das Abriegeln der Controller über Middlewares und das elegante Prüfen von Rechten in unseren Form Requests ($user->can()) ist die API gegen unautorisierte Zugriffe vollständig immun. Den finalen Schliff haben wir erreicht, indem wir diese komplexen Rechte-Arrays über eine API-Resource an unser Next.js Frontend gesendet haben, um dort dynamische, mitdenkende Benutzeroberflächen zu schaffen.
Teil der Serie
Headless CMS mit Laravel und Next.js
Headless CMS mit Laravel 12 & Next.js: Der ultimative Guide Pillar
Das perfekte Laravel Next.js Setup: Projektstruktur für dein Headless CMS
Datenbank-Design für ein skalierbares Headless CMS
Perfekte RESTful API mit Laravel 12 & API Resources bauen
Sichere Laravel Sanctum Next.js Authentifizierung bauen
Das ultimative Next.js Admin Dashboard mit shadcn/ui bauen
Rollen & Rechte-Management mit Laravel Spatie integrieren
Next.js App Router Architektur für Admin & Public
CRUD im Frontend: React Hook Form & Zod mit API
Datei-Uploads und Media-Management über die API
React Server Components für maximalen SEO-Boost
Häufig gestellte Fragen (FAQ)
Ausblick auf Teil 7: Next.js App Router Architektur für Admin & Public
Unsere Festung ist nun absolut kugelsicher. Jeder API-Endpunkt prüft strikt die Berechtigungen, und unser Admin-Dashboard passt sich völlig dynamisch und sicher den Rollen der jeweiligen Redakteure an. Aber ein Headless CMS dient letztendlich dazu, Inhalte an die Außenwelt auszuliefern!
Im nächsten Teil unserer Masterclass strukturieren wir unser Next.js-Frontend architektonisch komplett neu. Wir machen uns die wahren Stärken der Next.js App Router Architektur (Route Groups) zunutze und trennen unsere Anwendung in zwei völlig isolierte Welten: Den geschützten Admin-Bereich ((admin)), der auf dynamisches Rendering setzt, und das blitzschnelle, öffentliche Blog-Frontend ((public)), das wir mithilfe von Caching für deine Leser und für Google (SEO) auf maximale Performance trimmen. Wir bringen dein CMS live!
Hier geht es zu Teil 7: Next.js App Router Architektur für Admin & Public

Dietrich Bojko
Senior Webentwickler
Webinteger arbeitet seit vielen Jahren produktiv mit
Linux-basierten Entwicklungsumgebungen unter Windows.
Der Fokus liegt auf
performanten Setups mit WSL 2, Docker, PHP, Node.js und modernen
Build-Tools in realen Projekten –
nicht auf theoretischen Beispielkonfigurationen.
Die Artikel dieser Serie entstehen direkt aus dem täglichen Einsatz in Kunden- und Eigenprojekten und dokumentieren bewusst auch typische Fehler, Engpässe und bewährte Workarounds.


