Route meta para autenticación
Vue Router permite añadir metadatos a las rutas a través de la propiedad meta. Estos metadatos son accesibles en los navigation guards y permiten declarar requisitos de acceso de forma clara:
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
// Extender el tipado de RouteMeta para TypeScript
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
requiredRoles?: string[]
title?: string
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomePage.vue'),
meta: { title: 'Inicio' }
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginPage.vue'),
meta: { title: 'Iniciar sesión' }
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/RegisterPage.vue'),
meta: { title: 'Registro' }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/DashboardPage.vue'),
meta: { requiresAuth: true, title: 'Panel de control' }
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/ProfilePage.vue'),
meta: { requiresAuth: true, title: 'Mi perfil' }
},
{
path: '/admin/users',
name: 'AdminUsers',
component: () => import('@/views/admin/UsersPage.vue'),
meta: { requiresAuth: true, requiredRoles: ['admin'], title: 'Gestión de usuarios' }
},
{
path: '/admin/settings',
name: 'AdminSettings',
component: () => import('@/views/admin/SettingsPage.vue'),
meta: { requiresAuth: true, requiredRoles: ['admin'], title: 'Configuración' }
},
{
path: '/editor/articles',
name: 'EditorArticles',
component: () => import('@/views/editor/ArticlesPage.vue'),
meta: { requiresAuth: true, requiredRoles: ['admin', 'editor'], title: 'Artículos' }
},
{
path: '/unauthorized',
name: 'Unauthorized',
component: () => import('@/views/UnauthorizedPage.vue'),
meta: { title: 'Acceso denegado' }
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFoundPage.vue'),
meta: { title: 'Página no encontrada' }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
La declaración declare module 'vue-router' extiende la interfaz RouteMeta para que TypeScript reconozca las propiedades personalizadas requiresAuth y requiredRoles.
Guard global beforeEach
El guard beforeEach se ejecuta antes de cada navegación. Es el lugar ideal para implementar la lógica de autenticación y autorización:
// src/router/index.ts (continuación)
import { useAuthStore } from '@/stores/authStore'
router.beforeEach((to, from) => {
const authStore = useAuthStore()
// Actualizar el título de la página
document.title = (to.meta.title as string) || 'Mi Aplicación'
// Si la ruta no requiere autenticación, permitir acceso
if (!to.meta.requiresAuth) {
// Si el usuario ya está autenticado y va al login, redirigir al dashboard
if (to.name === 'Login' && authStore.isAuthenticated) {
return { name: 'Dashboard' }
}
return true
}
// Si la ruta requiere autenticación y el usuario no está autenticado
if (!authStore.isAuthenticated) {
return {
name: 'Login',
query: { redirect: to.fullPath }
}
}
// Si la ruta requiere roles específicos
if (to.meta.requiredRoles && to.meta.requiredRoles.length > 0) {
const userRole = authStore.userRole
const hasRequiredRole = to.meta.requiredRoles.includes(userRole)
if (!hasRequiredRole) {
return { name: 'Unauthorized' }
}
}
// El usuario está autenticado y tiene los permisos necesarios
return true
})
Este guard implementa tres niveles de verificación:
- Rutas públicas: se permite el acceso directo, pero si un usuario autenticado intenta ir al login, se redirige al dashboard.
- Autenticación: si la ruta requiere autenticación y el usuario no ha iniciado sesión, se redirige al login preservando la URL original como parámetro
redirect. - Autorización por roles: si la ruta requiere un rol específico y el usuario no lo tiene, se redirige a la página de acceso denegado.

Protección basada en roles
Para implementar un sistema de roles robusto, es útil crear funciones auxiliares que simplifiquen las verificaciones:
// src/utils/permissions.ts
import { useAuthStore } from '@/stores/authStore'
export function hasRole(role: string): boolean {
const authStore = useAuthStore()
return authStore.userRole === role
}
export function hasAnyRole(roles: string[]): boolean {
const authStore = useAuthStore()
return roles.includes(authStore.userRole)
}
export function isAdmin(): boolean {
return hasRole('admin')
}
Estas funciones se pueden utilizar tanto en guards como en componentes para mostrar u ocultar elementos según el rol:
<script setup lang="ts">
import { useAuthStore } from '@/stores/authStore'
import { computed } from 'vue'
const authStore = useAuthStore()
const isAdmin = computed(() => authStore.userRole === 'admin')
const canEdit = computed(() => ['admin', 'editor'].includes(authStore.userRole))
</script>
<template>
<nav>
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/profile">Perfil</router-link>
<router-link v-if="canEdit" to="/editor/articles">Artículos</router-link>
<router-link v-if="isAdmin" to="/admin/users">Usuarios</router-link>
<router-link v-if="isAdmin" to="/admin/settings">Configuración</router-link>
</nav>
</template>
Redirigir con URL de retorno
Un flujo esencial es preservar la URL que el usuario intentaba visitar antes de ser redirigido al login, y enviarlo de vuelta tras autenticarse correctamente:
<!-- LoginPage.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useAuthStore } from '@/stores/authStore'
const router = useRouter()
const route = useRoute()
const authStore = useAuthStore()
const email = ref('')
const password = ref('')
async function handleLogin() {
try {
await authStore.login({ email: email.value, password: password.value })
// Redirigir a la URL original almacenada en el query param
const redirectPath = route.query.redirect as string
if (redirectPath) {
router.push(redirectPath)
} else {
router.push({ name: 'Dashboard' })
}
} catch {
// Error gestionado por el store
}
}
</script>
<template>
<form @submit.prevent="handleLogin">
<div>
<label for="email">Email</label>
<input id="email" v-model="email" type="email" required />
</div>
<div>
<label for="password">Contraseña</label>
<input id="password" v-model="password" type="password" required />
</div>
<button type="submit" :disabled="authStore.loading">Entrar</button>
<p v-if="authStore.error">{{ authStore.error }}</p>
</form>
</template>
Guards por ruta: beforeEnter
Además del guard global, Vue Router permite definir guards específicos para rutas individuales con beforeEnter. Es útil para lógica que solo aplica a ciertas rutas:
const routes: RouteRecordRaw[] = [
{
path: '/admin/danger-zone',
name: 'DangerZone',
component: () => import('@/views/admin/DangerZonePage.vue'),
meta: { requiresAuth: true, requiredRoles: ['admin'] },
beforeEnter: (to, from) => {
const authStore = useAuthStore()
// Verificación adicional: solo el superadmin puede acceder
if (authStore.user?.email !== 'superadmin@empresa.com') {
return { name: 'Unauthorized' }
}
return true
}
},
{
path: '/setup',
name: 'Setup',
component: () => import('@/views/SetupPage.vue'),
beforeEnter: () => {
// Solo accesible si la app no está configurada
const isConfigured = localStorage.getItem('app-configured')
if (isConfigured) {
return { name: 'Home' }
}
return true
}
}
]
beforeEnter se ejecuta después de beforeEach. Acepta una función o un array de funciones que se ejecutan en orden.
Guards en componentes con Composition API
Dentro de los componentes, se pueden usar los hooks onBeforeRouteLeave y onBeforeRouteUpdate para reaccionar a cambios de navegación:
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
const hasUnsavedChanges = ref(false)
const formData = ref({ name: '', email: '' })
// Prevenir navegación si hay cambios sin guardar
onBeforeRouteLeave((to, from) => {
if (hasUnsavedChanges.value) {
const answer = window.confirm(
'¿Tienes cambios sin guardar. ¿Seguro que quieres salir?'
)
if (!answer) {
return false // Cancela la navegación
}
}
return true
})
// Reaccionar a cambios en parámetros de la misma ruta
onBeforeRouteUpdate((to, from) => {
// Por ejemplo, cuando cambia el ID en /users/:id
console.log(`Navegando de usuario ${from.params.id} a ${to.params.id}`)
})
function updateField() {
hasUnsavedChanges.value = true
}
function saveForm() {
// Lógica de guardado
hasUnsavedChanges.value = false
}
</script>
<template>
<form @submit.prevent="saveForm">
<input v-model="formData.name" @input="updateField" />
<input v-model="formData.email" @input="updateField" />
<button type="submit">Guardar</button>
</form>
</template>
Páginas de error
Es buena práctica tener páginas dedicadas para los casos de error de navegación:
UnauthorizedPage.vue - Acceso denegado:
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
function goBack() {
router.back()
}
function goHome() {
router.push({ name: 'Home' })
}
</script>
<template>
<div>
<h1>403 - Acceso denegado</h1>
<p>No tienes permisos para acceder a esta página.</p>
<div>
<button @click="goBack">Volver atrás</button>
<button @click="goHome">Ir al inicio</button>
</div>
</div>
</template>
NotFoundPage.vue - Página no encontrada:
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
</script>
<template>
<div>
<h1>404 - Página no encontrada</h1>
<p>La página que buscas no existe o ha sido movida.</p>
<button @click="router.push({ name: 'Home' })">Volver al inicio</button>
</div>
</template>
Resumen del flujo de navegación
El orden de ejecución de los guards en Vue Router es el siguiente:
- beforeRouteLeave del componente actual (si aplica).
- beforeEach global.
- beforeRouteUpdate del componente reutilizado (si aplica).
- beforeEnter de la ruta destino.
- Se resuelven los componentes asíncronos de la ruta.
- afterEach global (no puede cancelar la navegación).
Este orden permite implementar una estrategia de seguridad en capas, donde cada guard se encarga de un aspecto específico de la validación.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Vuejs
Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Cursos que incluyen esta lección
Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje