Code前端首页关于Code前端联系我们

Vue Router 怎么实现登录权限控制?

terry 2天前 阅读数 23 #Vue

做 Vue 项目时,不少同学都会碰到这样的需求:没登录的用户不能进个人中心、订单页这些私密页面;管理员和普通用户能访问的页面还不一样,这时候就得靠 Vue Router 的登录权限控制来实现,可具体咋操作?从基础逻辑到细节处理,咱一步步拆解清楚。

先理清登录权限控制的核心逻辑

权限控制本质是「拦请求 + 验身份 + 分权限」的组合拳,举个例子:用户想访问 /profile(个人中心),Vue Router 得先“拦住”这个请求,检查用户有没有登录、是不是有权限,再决定让不让进,核心靠这三部分配合:

  • 路由守卫:相当于项目里的“门卫”,所有页面跳转前先检查资格;
  • 身份验证:一般用 Token(令牌)判断用户是否登录,Token 可以存在 LocalStorage、SessionStorage 这类浏览器存储里;
  • 权限标记:给不同页面贴“标签”(比如哪些页面需要登录、哪些只有管理员能进),用路由的 meta 字段实现。

举个简单场景:用户点「我的订单」,路由守卫先查有没有 Token,没有就踢去登录页;有 Token 但订单页要求是 VIP 用户,还得查用户角色对不对,不对就跳 403 页面。

路由守卫是怎么拦截请求的?

Vue Router 提供了三种守卫:全局守卫路由独享守卫组件内守卫,开发里用得最多的是全局守卫 router.beforeEach,因为它能拦所有页面跳转。

(1)全局守卫:拦所有路由跳转

全局守卫是“全局门卫”——项目里所有页面跳转前,都会触发这个守卫,代码长这样:

// router/index.js
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})
router.beforeEach((to, from, next) => {
  // to:要跳转到的目标路由(包含路径、参数、meta 等信息)
  // from:从哪个路由跳过来的
  // next:决定往哪跳的函数,必须调用!不调用路由会卡住
  const isLogin = localStorage.getItem('token') // 假设登录成功后,把 token 存在 LocalStorage
  // 先看目标路由需不需要登录(用 meta.requiresAuth 标记)
  if (to.meta.requiresAuth) {
    if (isLogin) {
      next() // 已登录,放行
    } else {
      next('/login') // 没登录,踢去登录页
    }
  } else {
    next() // 不需要登录的页面,直接放行
  }
})

然后得给需要登录的路由加 meta 标记,比如路由配置:

const routes = [
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('@/views/Profile.vue'),
    meta: { requiresAuth: true } // 标记这个页面需要登录
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue')
  }
]

这样用户访问 /profile 时,全局守卫就会检查有没有 Token,没有就跳登录页。

(2)路由独享守卫:只拦单个路由

如果某个页面(比如管理员后台)想单独设置守卫,不用全局守卫,可以用 beforeEnter,比如管理员页面不仅要登录,还得验证角色:

{
  path: '/admin',
  name: 'Admin',
  component: () => import('@/views/Admin.vue'),
  meta: { requiresAuth: true }, 
  beforeEnter: (to, from, next) => {
    const userRole = localStorage.getItem('role') // 假设登录时存了用户角色
    if (userRole === 'admin') {
      next() // 是管理员,放行
    } else {
      next('/403') // 不是管理员,跳无权访问页
    }
  }
}

这种守卫只对 /admin 生效,适合页面权限更特殊的情况(比如只有超级管理员能进)。

(3)组件内守卫:组件里自己拦

如果想在组件内部处理权限(比如进入组件后,再判断用户有没有操作某个按钮的权限),可以用 beforeRouteEnter(进入组件前触发),比如在 Profile.vue 里:

export default {
  name: 'Profile',
  beforeRouteEnter(to, from, next) {
    const isLogin = localStorage.getItem('token')
    if (!isLogin) {
      next('/login') // 没登录就跳登录页
    } else {
      next() // 已登录放行
    }
  }
}

不过这种方式不如全局守卫灵活,一般用来做组件内的“额外验证”(比如进组件后,再查用户有没有编辑个人信息的权限)。

Token 该存在哪?怎么验证有效性?

Token 是判断用户是否登录的核心凭证,存哪、咋验证,得结合项目需求选。

(1)Token 的存储方式

常见的存储位置有三个,各有优劣:

  • LocalStorage:关闭浏览器也不会清除,适合“长期保持登录状态”的场景;但容易被 XSS 攻击(简单说就是黑客往页面插恶意脚本,偷 LocalStorage 里的 Token),所以别存密码这类敏感信息,只存 Token 就行。
  • SessionStorage:关闭标签页就清除,适合“临时登录”的场景;但页面刷新后还在,新开标签页就没了,比如做个临时预览功能,用 SessionStorage 存 Token 很合适。
  • Cookie:可以设置 httponly 防止 XSS,但前端拿 Cookie 麻烦,还可能有 CSRF 风险(比如黑客诱导用户点恶意链接,冒用 Cookie 发请求)。

大部分项目选 LocalStorage 存 Token,登录成功后存一下:

// 登录接口成功后
axios.post('/login', { username, password }).then(res => {
  const token = res.data.token
  localStorage.setItem('token', token) // 把 Token 存起来
  router.push('/profile') // 跳个人中心
})

(2)验证 Token 是否有效

存了 Token 不代表永远有效,得定期验证,常见做法有两种:

  • 全局守卫里“主动”验证:每次跳转前,用 Token 调后端接口(/api/verify),看是否过期,但这样每次跳转都发请求,影响性能,所以一般只在「页面刷新」或「登录后首次跳转」时验证。
  • 响应拦截器“被动”处理:后端接口返回 401(未授权)时,说明 Token 过期,直接跳登录页并清除 Token:
// axios 响应拦截器
axios.interceptors.response.use(
  response => response, // 响应正常,直接返回
  error => {
    if (error.response.status === 401) {
      localStorage.removeItem('token') // 清除过期 Token
      router.push('/login') // 跳登录页,让用户重新登录
    }
    return Promise.reject(error) // 把错误抛出去,让业务代码处理
  }
)

这样用户操作时,接口返回 401 就自动跳登录,体验更流畅。

动态加载路由实现细粒度权限

有些项目需要“不同角色看不同页面”,比如管理员能进用户管理,普通用户不能,这时候得用 动态路由——登录后根据角色加载对应路由。

(1)定义不同角色的路由表

先写好管理员和普通用户的路由(比如管理员能看用户管理,普通用户不能):

// 管理员专属路由
export const adminRoutes = [
  {
    path: '/user-manage',
    name: 'UserManage',
    component: () => import('@/views/Admin/UserManage.vue')
  }
]
// 普通用户路由(假设没有额外页面,这里只是示例)
export const userRoutes = []

(2)登录后动态添加路由

用户登录成功后,后端返回角色(res.data.role),前端根据角色加路由:

// 登录接口成功后
axios.post('/login', { username, password }).then(res => {
  const { token, role } = res.data
  localStorage.setItem('token', token)
  localStorage.setItem('role', role) // 存角色信息,后续验证用
  // 根据角色加载对应路由
  const asyncRoutes = role === 'admin' ? adminRoutes : userRoutes
  asyncRoutes.forEach(route => {
    router.addRoute(route) // 动态添加路由
  })
  router.push('/') // 跳首页,让动态路由生效
})

(3)处理动态路由的刷新问题

页面刷新后,动态添加的路由会“消失”(因为 Vue 重新初始化了),所以得在 全局守卫 里重新加载动态路由:

router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token')
  const role = localStorage.getItem('role')
  // 检查是否已添加动态路由(比如看 /user-manage 是否存在)
  const hasDynamicRoutes = router.getRoutes().some(route => route.path === '/user-manage') 
  if (isLogin && !hasDynamicRoutes) {
    // 已登录但没加动态路由,重新加
    const asyncRoutes = role === 'admin' ? adminRoutes : userRoutes
    asyncRoutes.forEach(route => {
      router.addRoute(route)
    })
    next({ ...to, replace: true }) // 重新跳转,确保路由生效
  } else {
    // 其他情况正常处理(比如检查是否需要登录)
    if (to.meta.requiresAuth && !isLogin) {
      next('/login')
    } else {
      next()
    }
  }
})

这样刷新页面后,动态路由会重新加载,用户不会看到 404。

登录状态持久化咋处理?

页面刷新时,Vue 实例会重新创建,之前存在内存里的登录状态会丢失,这时候得靠 LocalStorage/SessionStorage 存 Token,刷新后再读出来验证。

(1)刷新页面时恢复登录状态

在全局守卫里,页面刷新后先检查 LocalStorage 的 Token,再验证有效性:

router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token')
  if (to.meta.requiresAuth) {
    if (isLogin) {
      // 调后端接口验证 Token 是否真的有效(防止 Token 伪造)
      axios.get('/api/verify').then(res => {
        if (res.data.valid) {
          next() // Token 有效,放行
        } else {
          localStorage.removeItem('token') // 清除无效 Token
          next('/login') // 跳登录页
        }
      })
    } else {
      next('/login') // 没 Token,跳登录页
    }
  } else {
    next() // 不需要登录的页面,直接放行
  }
})

这样即使页面刷新,只要 Token 有效,用户还能保持登录状态。

(2)退出登录时清除状态

退出登录功能要彻底清除 Token 和角色信息,避免“退出后还能访问私密页面”:

// 退出登录按钮逻辑
const handleLogout = () => {
  localStorage.removeItem('token') // 清除 Token
  localStorage.removeItem('role') // 清除角色信息
  router.push('/login') // 跳登录页
  // 可选:调后端接口,让 Token 失效(比如加入黑名单)
  axios.post('/logout')
}

处理特殊场景:登录页重复跳转、权限错配

实际开发中,总有一些“意外情况”,得提前处理。

(1)已登录还跳登录页?

用户已经登录了,还点「登录」按钮,或者手动输 /login 网址,这时候得跳首页,别让他重复进登录页:

router.beforeEach((to, from, next) => {
  const isLogin = localStorage.getItem('token')
  // 如果要去登录页,但用户已登录
  if (to.path === '/login' && isLogin) {
    next('/') // 直接跳首页
    return // 终止后续逻辑
  }
  // 其他权限验证逻辑...
})

(2)权限错配导致 404?

比如用户是普通用户,却手动输管理员页面地址 /admin,前端路由守卫没拦住(meta 配置错了),这时候后端接口也会返回 403,前端要跳 403 页面:

// 路由配置里加 403 页面
{
  path: '/403',
  name: 'Forbidden',
  component: () => import('@/views/403.vue')
}
// axios 响应拦截器
axios.interceptors.response.use(
  response => response,
  error => {
    if (error.response.status === 403) {
      router.push('/403') // 跳无权访问页
    }
    return Promise.reject(error)
  }
)

(3)动态路由没加载导致白屏?

动态路由添加后,第一次访问可能因为路由还没加载完导致白屏,解决方法是用 next({ ...to, replace: true }) 强制刷新路由:

// 动态添加路由后
asyncRoutes.forEach(route => {
  router.addRoute(route)
})
next({ ...to, replace: true }) // 替换当前跳转,确保路由生效

结合后端接口做权限验证

前端控制路由只是“表面功夫”,后端必须再做一层验证!比如用户伪造 Token 访问管理员接口,前端拦不住,后端得检查 Token 里的角色权限。

(1)请求头带 Token

每次发请求时,把 Token 放到请求头里,让后端验证:

// axios 请求拦截器
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}` // 把 Token 塞到请求头
  }
  return config
})

(2)后端验证接口权限

后端接收到请求后,先解析 Token,检查用户角色是否能访问该接口。/api/user-manage 接口,只有管理员角色能访问,后端返回:

  • 200:有权限,返回数据;
  • 403:没权限,返回错误;
  • 401:Token 过期或无效。

前端再根据后端返回的状态码处理(403 跳 403 页面,401 跳登录页)。

实战案例:后台管理系统权限控制

假设做一个电商后台,有「普通运营」和「超级管理员」两种角色:

  • 普通运营:能看订单列表、商品列表;
  • 超级管理员:能看用户管理、系统设置,还能操作订单和商品。

(1)路由配置(基础 + 动态)

基础路由(不需要权限的页面):

const basicRoutes = [
  { path: '/', redirect: '/order' }, // 首页重定向到订单页
  { path: '/login', component: Login }, // 登录页
  { path: '/403', component: Forbidden } // 无权访问页
]

动态路由(按角色分):

// 运营路由(普通运营能看的页面)
export const operatorRoutes = [
  {
    path: '/order',
    component: Layout, // 布局组件,包含侧边栏、头部
    children: [
      { path: '', component: OrderList, meta: { requiresAuth: true } }
    ]
  },
  {
    path: '/product',
    component: Layout,
    children: [
      { path: '', component: ProductList, meta: { requiresAuth: true } }
    ]
  }
]
// 管理员路由(超级管理员能看的页面,包含运营的所有页面)
export const adminRoutes = [
  ...operatorRoutes, // 运营能看的,管理员也能看
  {
    path: '/user',
    component: Layout,
    children: [
      { path: '', component: UserManage, meta: { requiresAuth: true, roles: ['admin'] } }
    ]
  },
  {
    path: '/setting',
    component: Layout,
    children: [
      { path: '', component: SystemSetting, meta: { requiresAuth: true, roles: ['admin'] } }
    ]
  }
]

(2)登录逻辑 + 动态路由

登录页调接口,拿到角色后加路由:

// Login.vue
const handleLogin = () => {
  axios.post('/login', { username

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门