En proyectos web donde existen diferentes tipos de usuarios, una sola autenticación puede quedarse corta. Un caso común es cuando el sistema tiene un panel administrativo para colaboradores y, al mismo tiempo, una página publica donde los clientes pueden iniciar sesión para consultar o gestionar su información.
En este artículo veremos cómo estructurar en Laravel una autenticación separada para dos tipos de usuarios: administradores y clientes. La idea es mantener una arquitectura limpia, segura y fácil de mantener.
Contexto del proyecto
El escenario es el siguiente:
- El administrador accede desde una URL privada, por ejemplo:
/admin. - Los clientes acceden desde el sitio público, por ejemplo:
/. - Los administradores se almacenan en la tabla
users. - Los clientes se almacenan en la tabla
customers. - Cada sección usa vistas, layouts, assets y controladores separados.
- Se utiliza Laravel Breeze como punto de partida, pero se adapta la estructura para soportar dos autenticaciones independientes.
La decisión principal fue no mezclar administradores y clientes en una misma tabla, ya que tienen responsabilidades, permisos y flujos completamente diferentes.
Estructura recomendada del proyecto
Una estructura limpia puede organizarse así:
app/
├── Http/
│ ├── Controllers/
│ │ ├── Admin/
│ │ │ ├── Auth/
│ │ │ └── DashboardController.php
│ │ │
│ │ ├── Customer/
│ │ │ ├── Auth/
│ │ │ └── AccountController.php
│ │ │
│ │ └── Controller.php
│ │
│ ├── Requests/
│ │ ├── Admin/
│ │ │ └── Auth/
│ │ │ └── LoginRequest.php
│ │ │
│ │ └── Customer/
│ │ └── Auth/
│ │ └── LoginRequest.php
│
resources/
├── views/
│ ├── admin/
│ │ ├── auth/
│ │ ├── layouts/
│ │ └── dashboard/
│ │
│ └── customer/
│ ├── auth/
│ ├── layouts/
│ └── account/
│
routes/
├── admin.php
├── customer.php
├── web.php
└── console.php
public/
└── assets/
├── admin/
└── customer/
Esta separación permite que cada sección tenga su propia lógica, sus propios recursos visuales y sus propios middlewares.
Configuración de guards y providers
Laravel permite manejar múltiples sistemas de autenticación mediante guards y providers.
En este caso usamos:
webpara administradores.customerpara clientes.
La configuración se realiza en config/auth.php:
<?php
use App\Models\User;
use App\Models\Customer;
return [
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'customer' => [
'driver' => 'session',
'provider' => 'customers',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => User::class,
],
'customers' => [
'driver' => 'eloquent',
'model' => Customer::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
'customers' => [
'provider' => 'customers',
'table' => env('CUSTOMER_AUTH_PASSWORD_RESET_TOKEN_TABLE', 'customer_password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];
Un detalle importante es no usar la misma variable AUTH_MODEL para ambos providers, porque podría provocar que los clientes intenten autenticarse con el modelo de usuarios administrativos.
Modelo Customer autenticable
El modelo de cliente no debe extender directamente de Model, sino de Authenticatable, igual que el modelo User.
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Customer extends Authenticatable
{
use Notifiable;
protected $guard = 'customer';
protected $fillable = [
'name',
'last_name',
'phone',
'email',
'password',
'enabled',
];
protected $hidden = [
'password',
'remember_token',
];
}
Esto permite que Laravel pueda autenticar clientes usando el guard customer.
Migraciones principales
Para los clientes se puede crear una tabla independiente:
Schema::create('customers', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('last_name');
$table->char('phone', 10)->nullable();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->tinyInteger('enabled')->default(1);
$table->timestamps();
});
También se puede manejar una tabla independiente para recuperación de contraseñas:
Schema::create('customer_password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
En cuanto a sesiones, no es obligatorio crear una tabla diferente para clientes. Laravel usa la tabla sessions para guardar la sesión del navegador, no una sesión independiente por guard.
Rutas separadas
Una buena práctica es separar las rutas del administrador y del cliente.
Ejemplo de routes/admin.php:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Admin\DashboardController;
Route::middleware('admin.guest')->group(function () {
Route::get('/login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->name('login.store');
});
Route::middleware('admin.auth')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
Ejemplo de routes/customer.php:
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Customer\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Customer\HomeController;
use App\Http\Controllers\Customer\AccountController;
Route::get('/', [HomeController::class, 'index'])
->name('home');
Route::middleware('customer.guest')->group(function () {
Route::get('/login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('/login', [AuthenticatedSessionController::class, 'store'])
->name('login.store');
});
Route::middleware('customer.auth')->group(function () {
Route::get('/mi-cuenta', [AccountController::class, 'index'])
->name('account');
Route::post('/logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
Cargar rutas desde bootstrap/app.php
En Laravel moderno, las rutas adicionales pueden registrarse desde bootstrap/app.php:
use Illuminate\Support\Facades\Route;
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
then: function () {
Route::middleware('web')
->prefix('admin')
->name('admin.')
->group(base_path('routes/admin.php'));
Route::middleware('web')
->name('customer.')
->group(base_path('routes/customer.php'));
},
)
Con esta configuración se obtienen rutas como:
/admin/login→admin.login/admin/dashboard→admin.dashboard/login→customer.login/mi-cuenta→customer.account
Middlewares propios por tipo de usuario
Para evitar lógica condicional dentro de bootstrap/app.php, se pueden crear middlewares específicos para cada sección.
Por ejemplo:
admin.auth: valida que el usuario admin esté autenticado y activo.admin.guest: evita que un admin autenticado vuelva al login.customer.auth: valida que el cliente esté autenticado y activo.customer.guest: evita que un cliente autenticado vuelva al login.
Middleware de administrador autenticado:
<?php
namespace App\Http\Middleware\Admin;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnsureAdminIsAuthenticated
{
public function handle(Request $request, Closure $next): Response
{
if (! Auth::guard('web')->check()) {
return redirect()->route('admin.login');
}
$user = Auth::guard('web')->user();
if (! $user->enabled) {
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()
->route('admin.login')
->withErrors([
'email' => 'Tu cuenta se encuentra deshabilitada.',
]);
}
return $next($request);
}
}
Middleware de cliente autenticado:
<?php
namespace App\Http\Middleware\Customer;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class EnsureCustomerIsAuthenticated
{
public function handle(Request $request, Closure $next): Response
{
if (! Auth::guard('customer')->check()) {
return redirect()->route('customer.login');
}
$customer = Auth::guard('customer')->user();
if (! $customer->enabled) {
Auth::guard('customer')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()
->route('customer.login')
->withErrors([
'email' => 'Tu cuenta se encuentra deshabilitada.',
]);
}
return $next($request);
}
}
Después se registran los alias de middleware en bootstrap/app.php:
use Illuminate\Foundation\Configuration\Middleware;
use App\Http\Middleware\Admin\EnsureAdminIsAuthenticated;
use App\Http\Middleware\Admin\RedirectIfAdminAuthenticated;
use App\Http\Middleware\Customer\EnsureCustomerIsAuthenticated;
use App\Http\Middleware\Customer\RedirectIfCustomerAuthenticated;
->withMiddleware(function (Middleware $middleware): void {
$middleware->alias([
'admin.auth' => EnsureAdminIsAuthenticated::class,
'admin.guest' => RedirectIfAdminAuthenticated::class,
'customer.auth' => EnsureCustomerIsAuthenticated::class,
'customer.guest' => RedirectIfCustomerAuthenticated::class,
]);
})
LoginRequest para clientes
Uno de los errores más comunes al trabajar con múltiples guards es usar Auth::attempt() sin especificar guard. Cuando se hace eso, Laravel utiliza el guard por defecto, normalmente web.
Para el login de clientes debe usarse:
Auth::guard('customer')->attempt($credentials, $this->boolean('remember'))
Ejemplo:
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$credentials = $this->only('email', 'password');
$credentials['enabled'] = 1;
if (! Auth::guard('customer')->attempt($credentials, $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
Auth::guard('web')->logout();
RateLimiter::clear($this->throttleKey());
}
En este ejemplo, después de iniciar sesión como cliente, se cierra cualquier sesión activa del administrador en el mismo navegador.
LoginRequest para administradores
Para el administrador se hace lo mismo, pero usando el guard web:
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
$credentials = $this->only('email', 'password');
$credentials['enabled'] = 1;
if (! Auth::guard('web')->attempt($credentials, $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
Auth::guard('customer')->logout();
RateLimiter::clear($this->throttleKey());
}
De esta manera se evita que el mismo navegador mantenga una sesión de administrador y una sesión de cliente al mismo tiempo.
Uso correcto en Blade
Otro detalle importante es que la directiva @auth usa el guard por defecto si no se especifica uno. Por eso, en vistas de cliente se debe indicar explícitamente el guard.
Para clientes:
@auth('customer')
Hola, {{ auth('customer')->user()->name }}
@endauth
@guest('customer')
<a href="{{ route('customer.login') }}">Iniciar sesión</a>
@endguest
Para administradores:
@auth('web')
Hola, {{ auth('web')->user()->name }}
@endauth
@guest('web')
<a href="{{ route('admin.login') }}">Iniciar sesión</a>
@endguest
Sesiones en base de datos
Cuando se usa SESSION_DRIVER=database, Laravel guarda la sesión del navegador en la tabla sessions. Es importante entender que no se crea una sesión independiente por guard.
Si se inicia sesión como administrador y después como cliente en el mismo navegador, Laravel puede seguir usando el mismo registro de sesión. Lo importante es que el guard correcto esté autenticado y el otro haya sido cerrado.
Para validar:
auth('web')->check(); // Admin
auth('customer')->check(); // Cliente
También es normal que el registro en la tabla sessions permanezca por un tiempo aunque el usuario haya cerrado sesión. Laravel limpia las sesiones expiradas según su configuración de expiración.
Extras
Assets separados con Vite
Si cada sección utiliza un template diferente, también conviene separar los assets.
resources/
├── admin/
│ ├── css/
│ │ └── app.css
│ └── js/
│ └── app.js
│
└── customer/
├── css/
│ └── app.css
└── js/
└── app.js
Ejemplo de vite.config.js:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: [
'resources/admin/css/app.css',
'resources/admin/js/app.js',
'resources/customer/css/app.css',
'resources/customer/js/app.js',
],
refresh: true,
}),
],
});
En el layout de administrador:
@vite([
'resources/admin/css/app.css',
'resources/admin/js/app.js'
])
En el layout de cliente:
@vite([
'resources/customer/css/app.css',
'resources/customer/js/app.js'
])
Conclusión
Implementar autenticación para dos tipos de usuarios en Laravel requiere separar responsabilidades desde el inicio. La clave está en no mezclar modelos, guards, rutas ni vistas.
Una arquitectura limpia para este caso sería:
userspara administradores.customerspara clientes.webcomo guard de administradores.customercomo guard de clientes.resources/views/adminpara el panel administrativo.resources/views/customerpara el portal público.routes/admin.phpyroutes/customer.phppara separar rutas.- Middlewares propios para controlar accesos y redirecciones.
Esta estructura evita confusiones, mejora la seguridad y permite que el proyecto crezca de forma ordenada.
En sistemas donde existen usuarios internos y clientes finales, separar la autenticación no solo es una buena práctica técnica: también facilita el mantenimiento, las pruebas y la evolución del proyecto.







Deja un comentario