Tema
Middleware y autorización
Ya sabes quién es el usuario. Ahora toca decidir qué puede hacer. En esta lección crearás tus propios middleware (filtros que se ejecutan antes o después de cada petición) y usarás Gates y Policies para controlar permisos de forma ordenada: que un cliente sólo vea sus proyectos, que sólo un admin pueda borrar, etc.
Concepto
- Middleware: son capas por las que pasa la petición antes de llegar al controlador, como los filtros de seguridad de un aeropuerto. Cada uno puede dejarla pasar, modificarla o detenerla (por ejemplo, redirigir al login).
- Gate: una regla de permiso suelta, tipo "¿puede ver el panel de administración?".
- Policy: un conjunto de reglas para un modelo: "¿puede ver/editar/borrar este proyecto?".
txt
Petición → [middleware auth] → [middleware admin] → Controlador → (policy: ¿puede editar este post?) → RespuestaMiddleware
Crear un middleware
bash
php artisan make:middleware EnsureUserIsAdminphp
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureUserIsAdmin
{
/**
* $next($request) deja pasar la petición a la siguiente capa.
*/
public function handle(Request $request, Closure $next): Response
{
// Si no hay usuario o no es admin, cortamos aquí con 403
if (! $request->user()?->isAdmin()) {
abort(403, 'Sólo administradores.');
}
return $next($request);
}
}Registrarlo (Laravel 11/12)
En Laravel 11+ ya no existe app/Http/Kernel.php. Todo se configura en bootstrap/app.php:
php
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
// Alias para usarlo en rutas: ->middleware('admin')
$middleware->alias([
'admin' => \App\Http\Middleware\EnsureUserIsAdmin::class,
]);
// Añadir un middleware a TODAS las rutas web
$middleware->web(append: [
\App\Http\Middleware\SetLocaleFromUser::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();Uso en rutas:
php
Route::middleware(['auth', 'admin'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::get('/', [AdminController::class, 'index'])->name('dashboard');
Route::resource('users', Admin\UserController::class);
});
// También puedes usar la clase directamente sin alias
Route::get('/reportes', ReportController::class)->middleware(EnsureUserIsAdmin::class);Middleware con parámetros
Un solo middleware para varios roles:
php
class EnsureUserHasRole
{
// Los parámetros llegan después de $next: ->middleware('role:admin,editor')
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (! in_array($request->user()?->role, $roles, true)) {
abort(403);
}
return $next($request);
}
}php
$middleware->alias(['role' => \App\Http\Middleware\EnsureUserHasRole::class]);
Route::get('/blog/admin', ...)->middleware('role:admin,editor');Middleware "después" de la respuesta
php
public function handle(Request $request, Closure $next): Response
{
$response = $next($request); // primero deja que el controlador responda
// Luego modificamos la respuesta: añadir cabeceras de seguridad
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
return $response;
}Middleware integrados útiles: auth, guest, verified, throttle:60,1 (límite de peticiones), signed (URLs firmadas), can:update,post (autorización), password.confirm (pide la contraseña de nuevo antes de acciones sensibles).
Gates
Para permisos que no dependen de un modelo concreto. Se definen en App\Providers\AppServiceProvider:
php
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// ¿Puede ver el panel de administración?
Gate::define('view-admin', fn (User $user) => $user->role === 'admin');
// ¿Puede exportar reportes? (admins y contadores)
Gate::define('export-reports', fn (User $user) => in_array($user->role, ['admin', 'accountant']));
// Super admin: pasa todos los permisos
Gate::before(function (User $user, string $ability) {
if ($user->email === 'dueño@cliente.com') {
return true; // true = permitido, null = sigue evaluando normal
}
});
}
}Uso:
php
use Illuminate\Support\Facades\Gate;
// En un controlador: lanza 403 si no puede
Gate::authorize('export-reports');
// Como booleano
if (Gate::allows('view-admin')) { /* ... */ }
if ($request->user()->can('view-admin')) { /* ... */ }
// En rutas
Route::get('/admin', ...)->middleware('can:view-admin');Policies
Las policies agrupan los permisos de un modelo. Caso típico: cada usuario sólo puede editar y borrar sus propios proyectos.
bash
php artisan make:policy ProjectPolicy --model=Projectphp
<?php
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
class ProjectPolicy
{
/**
* Se ejecuta antes que cualquier otro método.
* Los admins pueden todo.
*/
public function before(User $user, string $ability): ?bool
{
return $user->role === 'admin' ? true : null; // null = decide el método normal
}
// Listado: cualquier usuario autenticado
public function viewAny(User $user): bool
{
return true;
}
// Ver uno: sólo el dueño
public function view(User $user, Project $project): bool
{
return $project->user_id === $user->id;
}
// Crear: usuarios con correo verificado
public function create(User $user): bool
{
return $user->hasVerifiedEmail();
}
public function update(User $user, Project $project): bool
{
return $project->user_id === $user->id;
}
// Borrar: el dueño, y sólo si el proyecto no está cerrado
public function delete(User $user, Project $project): bool
{
return $project->user_id === $user->id && $project->status !== 'closed';
}
}Laravel descubre la policy automáticamente si sigue la convención (App\Models\Project → App\Policies\ProjectPolicy). No hay que registrarla.
Usar la policy en el controlador
php
<?php
namespace App\Http\Controllers;
use App\Models\Project;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class ProjectController extends Controller
{
public function show(Project $project)
{
// Llama a ProjectPolicy::view(). Si devuelve false → 403
Gate::authorize('view', $project);
return view('projects.show', compact('project'));
}
public function create()
{
// Métodos sin modelo concreto: se pasa la clase
Gate::authorize('create', Project::class);
return view('projects.create');
}
public function update(Request $request, Project $project)
{
Gate::authorize('update', $project);
$project->update($request->validate([
'name' => ['required', 'max:255'],
]));
return back()->with('status', 'Proyecto actualizado.');
}
public function destroy(Project $project)
{
Gate::authorize('delete', $project);
$project->delete();
return redirect()->route('projects.index');
}
}Nota de versión
En Laravel 11+ el Controller base viene vacío. Si prefieres $this->authorize('update', $project), añade el trait Illuminate\Foundation\Auth\Access\AuthorizesRequests a app/Http/Controllers/Controller.php. Gate::authorize() funciona siempre sin configurar nada.
También puedes autorizar desde la ruta:
php
Route::put('/projects/{project}', [ProjectController::class, 'update'])
->middleware('can:update,project'); // "project" = nombre del parámetro de rutaO desde un Form Request:
php
public function authorize(): bool
{
return $this->user()->can('update', $this->route('project'));
}Usar la policy en Blade
Oculta botones que el usuario no puede usar:
blade
@can('update', $project)
<a href="{{ route('projects.edit', $project) }}">Editar</a>
@endcan
@can('delete', $project)
<form method="POST" action="{{ route('projects.destroy', $project) }}">
@csrf
@method('DELETE')
<button onclick="return confirm('¿Seguro?')">Eliminar</button>
</form>
@endcan
@cannot('create', App\Models\Project::class)
<p>Verifica tu correo para crear proyectos.</p>
@endcannotError grave
Ocultar el botón en Blade no es seguridad. Si sólo usas @can en la vista pero no autorizas en el controlador, cualquiera puede enviar la petición a mano (con Postman o la consola del navegador). Autoriza siempre en el servidor; @can es sólo para la interfaz.
Filtrar listados
Las policies deciden sobre un registro. Para listados, filtra con la consulta:
php
public function index(Request $request)
{
$projects = $request->user()->isAdmin()
? Project::latest()->paginate(15) // admin ve todo
: $request->user()->projects()->latest()->paginate(15); // el resto, sólo lo suyo
return view('projects.index', compact('projects'));
}Error común
Hacer Project::findOrFail($id) sin comprobar el dueño. Un usuario cambia el número en la URL (/projects/8 → /projects/9) y ve proyectos ajenos. A esta vulnerabilidad se le llama IDOR (referencia directa insegura) y es de las más comunes. Ver OWASP Top 10.
Respuestas con mensaje
php
use Illuminate\Auth\Access\Response;
public function delete(User $user, Project $project): Response
{
if ($project->status === 'closed') {
return Response::deny('No puedes borrar un proyecto cerrado.');
}
return $project->user_id === $user->id
? Response::allow()
: Response::denyAsNotFound(); // responde 404 para no revelar que existe
}Ejercicios
- Crea un middleware
EnsureAccountIsActiveque cierre la sesión y redirija al login con un mensaje si la columnais_activedel usuario esfalse. Regístralo para todas las rutas web. - Crea una
CommentPolicy: cualquiera autenticado puede crear; sólo el autor puede editar en los primeros 15 minutos; el autor o un admin pueden borrar.
Ver solución 1
php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnsureAccountIsActive
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user && ! $user->is_active) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login')
->withErrors(['email' => 'Tu cuenta está desactivada. Contacta al administrador.']);
}
return $next($request);
}
}php
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Http\Middleware\EnsureAccountIsActive::class,
]);
})Ver solución 2
php
<?php
namespace App\Policies;
use App\Models\Comment;
use App\Models\User;
class CommentPolicy
{
public function create(User $user): bool
{
return true;
}
public function update(User $user, Comment $comment): bool
{
return $comment->user_id === $user->id
&& $comment->created_at->gt(now()->subMinutes(15));
}
public function delete(User $user, Comment $comment): bool
{
return $comment->user_id === $user->id || $user->role === 'admin';
}
}Resumen
- Los middleware filtran peticiones; en Laravel 11/12 se registran en
bootstrap/app.phpconwithMiddleware(). - Usa alias (
'admin' => ...) y parámetros (role:admin,editor) para reutilizarlos. - Gates: permisos sueltos; Policies: permisos por modelo, descubiertas automáticamente.
- Autoriza con
Gate::authorize(),can:en rutas oauthorize()en Form Requests. @canen Blade sólo oculta la interfaz: la seguridad real está en el servidor.- Filtra listados por dueño para evitar IDOR.