El control de acceso basado en roles (RBAC) es utilizado en la mayoría de los sistemas para definir qué puede hacer cada usuario. En su versión más simple, conocida como RBAC plano, no es necesaria una tabla de permisos: cada usuario tiene un único rol, representado por un número, y ese número es el que determina qué se le permite hacer dentro del sistema.
En esta guía es construido un módulo de RBAC plano completo, con base de datos, conexión, lógica de validación y ejemplos de uso, usando solo PHP y MySQL. Si necesitas repasar los fundamentos antes de continuar, puedes consultar nuestra guía de PHP y MySQL.

1. Qué es el RBAC plano
En este modelo, cada rol es representado por un ID numérico. La regla que se sigue en esta guía es simple: entre más bajo el número, mayor es el nivel de acceso.
1 = admin (mayor nivel de acceso) 2 = subadmin 3 = encargado 4 = empleado (menor nivel de acceso)
Con esta lógica, validar “solo administradores o superiores” se reduce a una simple comparación: role_id <= 2.
2. Base de datos
Son necesarias únicamente dos tablas: rol y user. La columna role_id, dentro de user, es la que define el nivel de acceso de cada persona. Todo este bloque está guardado en el archivo schema.sql.
Cómo ejecutarlo: copia todo el bloque de código y pégalo directamente en tu consola de MySQL (o en phpMyAdmin / MySQL Workbench). Esto crea la base de datos
rbac_plano, sus tablas y los datos de ejemplo automáticamente.
-- schema.sql
CREATE DATABASE rbac_plano;
USE rbac_plano;
-- Tabla rol.
-- El ID es usado como nivel: entre más bajo, más privilegios.
CREATE TABLE rol (
id TINYINT UNSIGNED PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
-- Se insertan los 4 roles base del sistema.
INSERT INTO rol (id, name) VALUES
(1, 'admin'),
(2, 'subadmin'),
(3, 'encargado'),
(4, 'empleado');
-- Tabla user.
-- Cada usuario tiene un único role_id (no hay tabla de permisos).
CREATE TABLE user (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
role_id TINYINT UNSIGNED NOT NULL DEFAULT 4,
FOREIGN KEY (role_id) REFERENCES rol(id)
);Con esto, la base de datos ya queda lista para almacenar usuarios y asignarles un rol.
3. Conexión a la base de datos
La conexión es manejada con PDO, ya que permite el uso de consultas preparadas y un mejor control de errores frente a mysqli.
<?php
/**
* Clase Database.
* Se encarga únicamente de abrir la conexión a MySQL.
*/
class Database
{
// Aquí es guardada la conexión, para no abrirla más de una vez.
private static ?PDO $instance = null;
public static function getConnection(): PDO
{
// Si todavía no existe una conexión, es creada en este bloque.
if (self::$instance === null) {
$host = 'localhost';
$db = 'flat_rbac_demo';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
// Estas opciones son configuradas para que los errores
// sean lanzados como excepciones, no como warnings.
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
self::$instance = new PDO($dsn, $user, $pass, $options);
}
return self::$instance;
}
}Ajusta $host, $db, $user y $pass según tu entorno antes de continuar.
4. La clase Auth: el corazón del RBAC plano
Toda la validación de acceso es concentrada en una sola clase. Esto evita que el número de cada rol quede repetido y disperso por todo el proyecto.
Primero son definidas las constantes de cada rol, para no escribir números “mágicos” en el código:
<?php
class Auth
{
// Cada constante representa un rol y su nivel de acceso.
public const ROLE_ADMIN = 1;
public const ROLE_SUBADMIN = 2;
public const ROLE_ENCARGADO = 3;
public const ROLE_EMPLEADO = 4;
// ... los métodos son agregados a continuación
}
Después es agregado el método que verifica si el usuario tiene exactamente un rol:
/**
* Es verificado si el usuario logueado tiene un rol exacto.
*/
public static function hasRole(int $roleId): bool
{
return isset($_SESSION['role_id']) && $_SESSION['role_id'] === $roleId;
}
Luego es agregado el método más importante: minLevel(). Con él es validado si el usuario tiene el nivel mínimo requerido, o uno superior:
/**
* Es verificado si el usuario tiene un rol igual o "más alto"
* que el mínimo requerido. Como el ID más bajo equivale a más
* privilegios, la comparación usada es <=.
*
* Ejemplo: minLevel(2) permite el paso a admin (1) y subadmin (2),
* pero bloquea a encargado (3) y empleado (4).
*/
public static function minLevel(int $maxRoleId): bool
{
return isset($_SESSION['role_id']) && $_SESSION['role_id'] <= $maxRoleId;
}
Y finalmente, un método que detiene la ejecución cuando el nivel no es suficiente:
/**
* La ejecución es detenida si el usuario no cumple el nivel mínimo.
* Se usa al inicio de cualquier página que deba ser protegida.
*/
public static function requireMinLevel(int $maxRoleId): void
{
if (!self::minLevel($maxRoleId)) {
http_response_code(403);
die('No tienes permiso para acceder a esta sección.');
}
}
La clase completa queda así:
<?php
class Auth
{
public const ROLE_ADMIN = 1;
public const ROLE_SUBADMIN = 2;
public const ROLE_ENCARGADO = 3;
public const ROLE_EMPLEADO = 4;
public static function hasRole(int $roleId): bool
{
return isset($_SESSION['role_id']) && $_SESSION['role_id'] === $roleId;
}
public static function minLevel(int $maxRoleId): bool
{
return isset($_SESSION['role_id']) && $_SESSION['role_id'] <= $maxRoleId;
}
public static function hasAnyRole(array $roleIds): bool
{
return isset($_SESSION['role_id']) && in_array($_SESSION['role_id'], $roleIds, true);
}
public static function requireMinLevel(int $maxRoleId): void
{
if (!self::minLevel($maxRoleId)) {
http_response_code(403);
die('No tienes permiso para acceder a esta sección.');
}
}
}
5. Login: el rol es guardado en la sesión
Al iniciar sesión, el role_id del usuario es leído de la base de datos una sola vez y es guardado en $_SESSION. De esta forma, no es necesario consultar la base de datos cada vez que se valida un permiso.
<?php
session_start();
require_once __DIR__ . '/Database.php';
// Este bloque solo es ejecutado cuando el formulario es enviado.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$db = Database::getConnection();
// La consulta es preparada para evitar inyección SQL.
$stmt = $db->prepare("SELECT id, name, password, role_id FROM user WHERE email = :email");
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();
// La contraseña es verificada contra el hash guardado en la base de datos.
if ($user && password_verify($password, $user['password'])) {
// Los datos del usuario, incluyendo su rol, son guardados en sesión.
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_name'] = $user['name'];
$_SESSION['role_id'] = (int) $user['role_id'];
header('Location: admin.php');
exit;
}
$error = 'Correo o contraseña incorrectos.';
}
Nota importante: las contraseñas nunca deben guardarse en texto plano. Al crear un usuario nuevo, la contraseña debe ser generada con password_hash($password, PASSWORD_DEFAULT).
6. Cómo proteger una página
Con la clase Auth ya construida, cualquier página puede ser protegida en una sola línea:
<?php session_start(); require_once __DIR__ . '/Auth.php'; // Solo los usuarios con rol admin (1) o subadmin (2) pueden continuar. Auth::requireMinLevel(Auth::ROLE_SUBADMIN); // A partir de aquí, el resto del contenido de la página.
También es posible mostrar u ocultar partes del HTML según el rol del usuario:
<?php if (Auth::hasRole(Auth::ROLE_ADMIN)): ?>
<button>Eliminar usuario</button>
<?php endif; ?>
Con estas dos formas de uso —bloquear el acceso completo a una página, o mostrar elementos específicos— queda cubierta la mayoría de los casos reales de un sistema con roles.
7. Ventajas y límites de este modelo
A favor de este modelo:
- Es implementado en pocos minutos.
- Es fácil de entender, incluso para desarrolladores junior.
- No es necesaria ninguna consulta adicional a la base de datos después del login.
En contra, o cuándo conviene migrar a un RBAC completo:
- Si son necesarios permisos específicos por módulo (por ejemplo, “puede editar pero no eliminar”), el ID de rol se queda corto.
- Si los roles cambian con frecuencia, o si deben crearse roles personalizados sin tocar código, es recomendable usar una tabla de permisos separada.
- Si un mismo usuario necesita más de un rol a la vez, este modelo no lo permite, ya que cada usuario tiene un único
role_id.
Descargar
Todos los archivos de esta guía (schema.sql, Database.php, Auth.php, login.php y admin.php) están disponibles listos para usar:
⬇ Descargar código fuente (rbac-plano-v2.zip)
Recuerda ajustar las credenciales de conexión en
Database.phpantes de probarlo en tu servidor.
Instalación: cómo probar la demo
Requisitos: un servidor local con PHP 8+ y MySQL 8+ (por ejemplo XAMPP, Laragon o MAMP).
- Descarga y descomprime el archivo
.zipdel apartado anterior. Quedará una carpeta con tres subcarpetas:sql/,src/ypublic/. - Importa la base de datos. Copia todo el contenido de
sql/schema.sqly pégalo en tu consola de MySQL (o en phpMyAdmin), tal como se explicó en el paso 2 de esta guía. Esto crea la base de datosrbac_planocon sus tablas y dos usuarios de prueba ya cargados. - Configura la conexión. Abre
src/Database.phpy ajusta$host,$usery$passsegún tu entorno local (por defecto ya apunta arbac_planocon usuariorooty sin contraseña, la configuración típica de XAMPP/Laragon). - Coloca el proyecto en tu servidor local. Mueve la carpeta completa (
sql/,src/ypublic/) dentro del directorio que tu servidor sirve (por ejemplohtdocsen XAMPP owwwen Laragon). - Abre la demo en el navegador, apuntando a
public/login.php(por ejemplohttp://localhost/rbac-plano/public/login.php).
Inicia sesión con cualquiera de los usuarios de prueba:
| Correo | Contraseña | Rol |
|---|---|---|
| admin@demo.com | admin123 | admin |
| juan@demo.com | empleado123 | empleado |
Verifica el funcionamiento: con admin@demo.com deberías entrar sin problema a admin.php y ver la sección exclusiva de administrador. Si cierras sesión e ingresas con juan@demo.com, el sistema debe bloquear el acceso con un error 403, ya que su rol (empleado, nivel 4) no cumple el nivel mínimo requerido (subadmin, nivel 2).
Conclusión
El RBAC plano es la opción correcta cuando el sistema tiene pocos niveles de acceso, bien definidos y estables en el tiempo. No es necesario complicar la base de datos con tablas de permisos si el negocio no lo requiere todavía. Cuando esos límites empiecen a sentirse, siempre es posible dar el salto a un RBAC completo con tabla de permisos, sin perder la lógica ya construida aquí.
Si en cambio ya necesitas un sistema con roles, múltiples sucursales y control de acceso ya resuelto, sin programarlo desde cero, puedes conocer Inventio Max.