Guardias de rutas en Vue 3 para acceso por roles: patrones prácticos
Guardias de rutas en Vue 3 para acceso por roles explicados con patrones prácticos: reglas en `meta` de rutas, redirecciones seguras, fallbacks 401/403 amigables y cómo evitar fugas de datos.

Qué solucionan realmente los guardias de ruta (y qué no hacen)
Los guardias de ruta hacen bien una cosa: controlan la navegación. Deciden si alguien puede entrar en una ruta y a dónde enviarlo si no puede. Eso mejora la experiencia de usuario, pero no es lo mismo que seguridad.
Ocultar un elemento de menú es solo una pista, no autorización. La gente aún puede escribir una URL, refrescar en un enlace profundo o abrir un marcador. Si tu única protección es «el botón no es visible», no tienes protección.
Los guardias destacan cuando quieres que la app se comporte de forma coherente al bloquear páginas que no deberían mostrarse, como áreas de administración, herramientas internas o portales de clientes por roles.
Los guardias te ayudan a:
- Bloquear páginas antes de que se rendericen
- Redirigir al login o a un destino seguro
- Mostrar una pantalla clara 401/403 en lugar de una vista rota
- Evitar bucles de navegación accidentales
Lo que los guardias no pueden hacer es proteger los datos por sí solos. Si una API devuelve datos sensibles al navegador, un usuario aún puede llamar a ese endpoint directamente (o inspeccionar respuestas en las herramientas de desarrollo) aunque la página esté bloqueada. La autorización real debe ocurrir también en el servidor.
Un buen objetivo es cubrir ambos lados: bloquear páginas y bloquear datos. Si un agente de soporte abre una ruta solo para admins, el guardia debe detener la navegación y mostrar “Acceso denegado”. Por separado, tu backend debe rechazar llamadas API exclusivas de admin para que los datos restringidos nunca se devuelvan.
Elige un modelo simple de roles y permisos
El control de acceso se complica cuando empiezas con una larga lista de roles. Comienza con un conjunto pequeño que la gente realmente entienda y añade permisos más finos solo cuando notes dolor real.
Una división práctica es:
- Los roles describen quién es alguien en tu app.
- Los permisos describen lo que puede hacer.
Para la mayoría de herramientas internas, tres roles cubren mucho:
- admin: gestiona usuarios y ajustes, ve todos los datos
- support: maneja registros y respuestas de clientes, pero no ajustes del sistema
- viewer: acceso de solo lectura a pantallas aprobadas
Decide desde temprano de dónde vienen los roles. Las reclamaciones en tokens (JWT) son rápidas para los guardias pero pueden quedarse desactualizadas hasta que se refrescan. Obtener el perfil de usuario al iniciar la app siempre está actualizado, pero tus guardias deben esperar a que esa petición termine.
También separa claramente los tipos de rutas: rutas públicas (abiertas para todos), rutas autenticadas (requieren sesión) y rutas restringidas (requieren un rol o permiso).
Define reglas de acceso con meta de ruta
La forma más limpia de expresar el acceso es declararlo en la propia ruta. Vue Router te permite adjuntar un objeto meta a cada registro de ruta para que los guardias lo lean después. Esto mantiene las reglas cerca de las páginas que protegen.
Elige una forma simple para meta y úsala de manera coherente en la app.
const routes = [
{
path: "/admin",
component: () => import("@/pages/AdminLayout.vue"),
meta: { requiresAuth: true, roles: ["admin"] },
children: [
{
path: "users",
component: () => import("@/pages/AdminUsers.vue"),
// hereda requiresAuth + roles del padre
},
{
path: "audit",
component: () => import("@/pages/AdminAudit.vue"),
meta: { permissions: ["audit:read"] },
},
],
},
{
path: "/tickets",
component: () => import("@/pages/Tickets.vue"),
meta: { requiresAuth: true, permissions: ["tickets:read"], readOnly: true },
},
]
Para rutas anidadas, decide cómo se combinan las reglas. En la mayoría de apps, los hijos deberían heredar los requisitos del padre. En tu guardia, comprueba cada registro de ruta matcheado (no solo to.meta) para que no se omitan las reglas del padre.
Un detalle que ahorra tiempo más adelante: diferencia entre “puede ver” y “puede editar”. Una ruta puede ser visible para support y admins, pero las ediciones deberían desactivarse para support. Una bandera readOnly: true en meta puede controlar el comportamiento de la UI (desactivar acciones, ocultar botones destructivos) sin pretender que sea seguridad.
Prepara el estado de auth para que los guardias se comporten de forma fiable
La mayoría de bugs de guardias vienen por un problema: el guardia se ejecuta antes de que la app sepa quién es el usuario.
Trata la autenticación como una pequeña máquina de estados y hazla la única fuente de verdad. Quieres tres estados claros:
- unknown: la app acaba de arrancar, la sesión no se ha comprobado aún
- logged out: la comprobación de sesión terminó, no hay usuario válido
- logged in: usuario cargado, roles/permiso disponibles
La regla: nunca leas roles mientras la auth esté en unknown. Ahí es donde obtienes destellos de pantallas protegidas o redirecciones sorpresa al login.
Decide cómo funciona el refresh de sesión
Elige una estrategia de refresco y mantenla predecible (por ejemplo: leer un token, llamar a un endpoint “who am I”, establecer el usuario).
Un patrón estable se ve así:
- Al cargar la app, marca auth como unknown y lanza una única petición de refresh
- Resuelve los guardias solo después de que el refresh termine (o expire)
- Cachea el usuario en memoria, no en
metade ruta - En fallo, marca auth como logged out
- Expón una promesa
ready(o similar) que los guardias puedan await
Una vez en su lugar, la lógica del guardia se mantiene simple: espera a que auth esté lista y luego decide el acceso.
Paso a paso: implementar autorización a nivel de ruta
Un enfoque limpio es mantener la mayoría de reglas en un guard global y usar guardias por ruta solo cuando una ruta necesita lógica especial.
1) Añade un guard global beforeEach
// router/index.js
router.beforeEach(async (to) => {
const auth = useAuthStore()
// Paso 2: esperar la inicialización de auth cuando sea necesario
if (!auth.ready) await auth.init()
// Paso 3: comprobar autenticación, luego roles/permiso
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
const roles = to.meta.roles
if (roles && roles.length > 0 && !roles.includes(auth.userRole)) {
return { name: 'forbidden' } // 403
}
// Paso 4: permitir la navegación
return true
})
Esto cubre la mayoría de casos sin dispersar comprobaciones por los componentes.
Cuándo beforeEnter encaja mejor
Usa beforeEnter cuando la regla sea genuinamente específica de la ruta, como “solo el propietario del ticket puede abrir esta página” y dependa de to.params.id. Mantenla corta y reutiliza la misma store de auth para que el comportamiento sea coherente.
Redirecciones seguras sin abrir agujeros
Las redirecciones pueden anular silenciosamente tu control de acceso si las tratas como confiables.
El patrón común es: cuando un usuario está desconectado, mándalo al login e incluye un parámetro returnTo. Tras el login, léelo y navega allí. El riesgo es el open redirect (enviar a usuarios a destinos no deseados) y los bucles.
Mantén un comportamiento simple:
- Usuarios desconectados van a
LoginconreturnToestablecido en la ruta actual. - Usuarios autenticados pero no autorizados van a una página
Forbiddendedicada (no aLogin). - Solo permite valores
returnTointernos que reconozcas. - Añade una comprobación de bucle para no redirigir al mismo sitio.
const allowedReturnTo = (to) => {
if (!to || typeof to !== 'string') return null
if (!to.startsWith('/')) return null
// opcional: solo permitir prefijos conocidos
if (!['/app', '/admin', '/tickets'].some(p => to.startsWith(p))) return null
return to
}
router.beforeEach((to) => {
if (!auth.isReady) return false
if (!auth.isLoggedIn && to.name !== 'Login') {
return { name: 'Login', query: { returnTo: to.fullPath } }
}
if (auth.isLoggedIn && !canAccess(to, auth.user) && to.name !== 'Forbidden') {
return { name: 'Forbidden' }
}
})
Evitar filtrar datos restringidos durante la navegación
La fuga más fácil es cargar datos antes de saber si el usuario puede verlos.
En Vue, esto suele pasar cuando una página hace fetch en setup() y el guard se ejecuta un momento después. Aunque el usuario sea redirigido, la respuesta puede aterrizar en un store compartido o aparecer brevemente en pantalla.
Una regla más segura es: autorizar primero, luego cargar.
// guard del router: autorizar antes de entrar a la ruta
router.beforeEach(async (to) => {
await auth.ready() // asegurar que se conocen los roles
const required = to.meta.requiredRole
if (required && !auth.hasRole(required)) {
return { name: 'forbidden' }
}
})
También presta atención a peticiones tardías cuando la navegación cambia rápido. Cancela peticiones (por ejemplo con AbortController) o ignora respuestas tardías comprobando un id de petición.
El cache es otra trampa común. Si guardas “el último registro de cliente cargado” globalmente, una respuesta solo para admin puede mostrarse después a un no-admin que visite la misma estructura de pantalla. Llavea caches por id de usuario y rol, y limpia módulos sensibles al cerrar sesión (o cuando cambian los roles).
Unos hábitos previenen la mayoría de fugas:
- No solicites datos sensibles hasta confirmar la autorización.
- Clavea datos cacheados por usuario y rol, o mantenlos locales a la página.
- Cancela o ignora peticiones en curso cuando cambia la ruta.
Fallbacks amigables: 401, 403 y not found
Los caminos de “no” importan tanto como los de “sí”. Buenas páginas de fallback mantienen orientados a los usuarios y reducen peticiones de soporte.
401: Login requerido (no autenticado)
Usa 401 cuando el usuario no esté firmado. Mantén el mensaje claro: necesita iniciar sesión para continuar. Si permites volver a la página original tras el login, valida la ruta de retorno para que no pueda apuntar fuera de tu app.
403: Acceso denegado (autenticado, pero no permitido)
Usa 403 cuando el usuario ha iniciado sesión pero carece de permiso. Mantén el tono neutral y evita dar pistas sobre detalles sensibles.
Una 403 sólida suele tener un título claro (“Acceso denegado”), una frase breve de explicación y un siguiente paso seguro (volver al panel, contactar a un admin, cambiar de cuenta si procede).
404: No encontrado
Maneja 404 por separado de 401/403. Si no, la gente asume que le falta permiso cuando la página simplemente no existe.
Errores comunes que rompen el control de acceso
La mayoría de bugs de control de acceso son deslices lógicos simples que aparecen como bucles de redirección, destellos de la página equivocada o usuarios atrapados.
Los culpables habituales:
- Tratar la UI oculta como “seguridad”. Siempre aplica roles en el router y en la API.
- Leer roles desde un estado obsoleto tras logout/login.
- Redirigir usuarios no autorizados a otra ruta protegida (bucle instantáneo).
- Ignorar el momento en que la auth aún se está cargando al refrescar.
- Confundir 401 y 403, lo que genera confusión.
Un ejemplo realista: un agente de soporte cierra sesión y un admin inicia sesión en el mismo equipo compartido. Si tu guard lee un rol cacheado antes de confirmar la nueva sesión, puedes bloquear al admin incorrectamente o, peor, permitir acceso brevemente que no deberías.
Lista de verificación rápida antes de lanzar
Haz una pasada corta que se enfoque en los momentos donde suele fallar el control de acceso: redes lentas, sesiones expiradas y URLs guardadas.
- Cada ruta protegida tiene requisitos
metaexplícitos. - Los guardias manejan el estado de carga de auth sin mostrar UI protegida brevemente.
- Usuarios no autorizados aterrizan en una página 403 clara (no en un rebotado confuso al inicio).
- Cualquier redirección de “volver a” (
returnTo) está validada y no puede crear bucles. - Las llamadas API sensibles se ejecutan solo tras confirmar la autorización.
Luego prueba un escenario de extremo a extremo: abre una URL protegida en una pestaña nueva estando desconectado, inicia sesión como usuario básico y confirma que aterrizas en la página indicada (si está permitido) o en una 403 limpia con un siguiente paso.
Ejemplo: acceso support vs admin en una pequeña web
Imagina una app de helpdesk con dos roles: support y admin. Support puede leer y responder tickets. Admin también puede eso, además de gestionar facturación y ajustes de la compañía.
/tickets/:idestá permitido parasupportyadmin/settings/billingestá permitido solo paraadmin
Ahora un momento común: un agente support abre un deep link a /settings/billing desde un marcador antiguo. El guard debe comprobar meta de la ruta antes de que la página cargue y bloquear la navegación. Como el usuario está autenticado pero no tiene el rol, debe aterrizar en un fallback seguro (403).
Dos mensajes importan:
- Login requerido (401): “Por favor, inicie sesión para continuar.”
- Acceso denegado (403): “No tiene acceso a Configuración de Facturación.”
Lo que no debe ocurrir: que el componente de facturación se monte o que se soliciten datos de facturación, ni siquiera brevemente.
Los cambios de rol a mitad de sesión son otro caso borde. Si alguien se promueve o degrada, no confíes en el menú. Revisa roles en navegación y decide cómo manejar las páginas activas: refresca el estado de auth cuando cambie el perfil, o detecta cambios de rol y redirige fuera de páginas que ya no están permitidas.
Próximos pasos: mantener las reglas de acceso manejables
Una vez que los guardias funcionen, el riesgo mayor es la deriva: una nueva ruta se publica sin meta, un rol se renombra y las reglas se vuelven inconsistentes.
Convierte tus reglas en un pequeño plan de pruebas que puedas ejecutar cada vez que añadas una ruta:
- Como Invitado: abre rutas protegidas y confirma que aterrizas en login sin ver contenido parcial.
- Como Usuario: abre una página que no deberías ver y confirma que recibes una 403 clara.
- Como Admin: prueba deep links copiados de la barra de direcciones.
- Para cada rol: refresca en una ruta protegida y confirma que el resultado es estable.
Si quieres una capa extra de seguridad, añade una vista de desarrollo o una salida por consola que liste rutas y sus requisitos meta, para que las reglas faltantes destaquen inmediatamente.
Si estás construyendo herramientas internas o portales con AppMaster (appmaster.io), puedes aplicar el mismo enfoque: mantiene los guardias de ruta enfocados en la navegación en la UI de Vue3 y aplica permisos donde vive la lógica y los datos en el backend.
Elige una mejora y llévala de extremo a extremo: endurecer el control de carga de datos, mejorar la página 403 o asegurar el manejo de redirecciones. Las pequeñas correcciones son las que detienen la mayoría de bugs reales en producción.
FAQ
Los guardias de ruta controlan la navegación, no el acceso a los datos. Te ayudan a bloquear una página, redirigir y mostrar un estado 401/403 limpio, pero no impiden que alguien llame a tu API directamente. Refuerza siempre las mismas reglas en el backend para que los datos restringidos nunca se devuelvan.
Ocultar elementos de la interfaz solo cambia lo que alguien ve, no lo que puede solicitar. Los usuarios aún pueden escribir una URL, abrir marcadores o usar deep links. Necesitas comprobaciones en el router para bloquear la página y autorización en el servidor para bloquear los datos.
Empieza con un conjunto pequeño y comprensible, y añade permisos cuando realmente lo necesites. Una base común es admin, support y viewer, y luego permisos como tickets:read o audit:read para acciones específicas. Mantén separado “quién eres” (rol) de “qué puedes hacer” (permiso).
Coloca las reglas de acceso en meta de los registros de ruta, por ejemplo requiresAuth, roles y permissions. Esto mantiene las reglas junto a las páginas y hace que el guard global sea predecible. Para rutas anidadas, comprueba todos los registros en to.matched para no saltarte requisitos del padre.
Lee desde to.matched y combina los requisitos de todos los registros emparejados. Así un hijo no podrá evitar el requiresAuth o roles del padre. Decide una regla clara de combinación desde el inicio (usualmente: los requisitos del padre se aplican a los hijos).
Sucede porque el guard puede ejecutarse antes de que la app sepa quién es el usuario. Trata la auth como tres estados—unknown, logged out, logged in—y nunca evalúes roles mientras la auth esté unknown. Haz que los guardias esperen una inicialización (por ejemplo, una petición “who am I”) antes de decidir.
Por defecto, usa un beforeEach global para reglas coherentes como “requiere login” o “requiere rol/permiso”. Usa beforeEnter solo cuando la regla dependa realmente de parámetros de la ruta (por ejemplo, “solo el propietario del ticket puede abrir esta página”). Mantén ambas vías usando la misma fuente de verdad de auth.
Trata returnTo como entrada no fiable. Solo permite rutas internas que reconozcas (por ejemplo, valores que empiecen por / y coincidan con prefijos conocidos) y añade una verificación de bucle para no redirigir de nuevo a la misma ruta bloqueada. Usuarios desconectados van a Login; usuarios autenticados pero no autorizados van a una página 403 dedicada.
Autoriza antes de solicitar. Si una página hace fetch en setup() y luego se redirige, la respuesta puede acabar en un store o aparecer brevemente. Pide confirmación de autorización antes de las solicitudes sensibles, y cancela o ignora peticiones en curso cuando cambia la ruta.
Usa 401 cuando el usuario no está autenticado y 403 cuando sí lo está pero no tiene permiso. Mantén 404 separado para que la gente no confunda una ruta inexistente con falta de permisos. Fallos claros y consistentes reducen la confusión y las peticiones de soporte.


