Rutas protegidas y guards

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

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:

  1. Rutas públicas: se permite el acceso directo, pero si un usuario autenticado intenta ir al login, se redirige al dashboard.
  2. 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.
  3. 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.

Guards de navegación: verificación de auth, token y roles antes de acceder

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:

  1. beforeRouteLeave del componente actual (si aplica).
  2. beforeEach global.
  3. beforeRouteUpdate del componente reutilizado (si aplica).
  4. beforeEnter de la ruta destino.
  5. Se resuelven los componentes asíncronos de la ruta.
  6. 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 - Autor del tutorial

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