Vite+Vue3+Pinia做页面的权限管理

1,314 阅读4分钟

权限管理

权限管理应该是每一个后台管理系统最重要的一部分,主要分为页面权限与按钮权限。 本文主要讲解页面权限的简单实现方式。

解决思路

首先我们要把页面分成三种类型:

  1. 任何人,包括游客(无需登录)就可以访问的页面(e.g. 登录页,指南页)
  2. 登录后人人都可以访问的页面(e.g. 系统首页)
  3. 登录后,且拥有该页面权限的人才可以访问的页面 (e.g. 数据列表页,权限管理页)

可以总结为下图: image.png

代码结构

分析完后,就可以开始构思代码了

  • 第一步: 先将路由分为 静态路由 staticRoutes 和 动态路由 dynamicRoutes 用上图的六个页面举例子

src/router/index.js

import { createRouter, createWebHashHistory } from 'vue-router'

/* Layout */
import Layout from '@/layout/index.vue'

/* staticRoutes */
export const staticRoutes = [
  {
    path: '/',
    hidden: true,
    redirect: '/home',
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import('@/views/HomePage/index.vue'),
    meta: { title: '首页' },
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login/index.vue'),
    meta: { title: '登录' },
  },
  {
    path: '/handbook',
    name: 'Handbook',
    component: () => import('@/views/CityMap/index.vue'),
    meta: { title: '用户手册' },
  },
]
/**
 * dynamicRoutes
 * the routes that need to be dynamically loaded based on user module
 */
export const dynamicRoutes = [
  {
    component: Layout,
    hidden: true,
    children: [
      {
        path: '/data',
        name: 'Data',
        component: () => import('@/views/Data/index.vue'),
        meta: { title: '数据列表' },
      },
      {
        path: '/permission',
        name: 'Permission',
        component: () => import('@/views/Permission/index.vue'),
        meta: { title: '权限配置' },
      },
    ],
  },
]

const router = createRouter({
  // hash模式配置
  history: createWebHashHistory(),
  routes: staticRoutes,
})

export default router

  • 第二步: 设置 全局前置守卫(beforEach)

使用全局前置守卫去作为一个拦截器这样的角色,拦截每一次的路由跳转变化,在去下一个路由之前,做出一系列的判断。

src/router/permission.js

import { useUserStore, usePermissionStore } from '@/store'
import { getToken } from '@/utils/auth'; 

//路由白名单列表,把路由添加到这个数组,不用登陆也可以访问
const whiteList = ['/login', '/handbook']

export function setBeforeEach(router) {
  router.beforeEach(async (to, from, next) => {
    // 设置标题
    document.title = to.meta.title
    
    //获取token
    const hasToken = getToken()
    const userStore = useUserStore()
    const permissionStore = usePermissionStore()

    //如果存在token,即存在已登陆的令牌
    if (hasToken) {
      //如果用户存在令牌的情况请求登录页面,就让用户直接跳转到首页,避免存在重复登录的情况
      if (to.path === '/login') {
        // 直接跳转到首页,取决于你的路由重定向到哪里
        next({ path: '/' })
      } else {
        //如果已经有令牌的用户请求的不是登录页,是其他页面
        //就从store里拿到用户的信息,这里也证明用户不是第一次登录了
        const isGetUserInfo = permissionStore.isGetUserInfo

        if (isGetUserInfo) {
          //信息拿到后,用户请求哪就跳转哪
          next()
        } else {
          try {
            // 如果有令牌,但是没有用户信息,证明用户是第一次登录,需要先设置用户信息
            let accessRoutes = []

            // 获取用户拥有的权限模块数组,从接口获取
            // note: module must be a array! such as: ['数据列表'] or ,['数据列表','权限管理']
            const { module } = await userStore.getInfo()

            accessRoutes = permissionStore.generateRoutes(module)
            // 动态添加可访问的路由
            accessRoutes.forEach(route => {
              router.addRoute(route)
            })
            // 把动态路由和静态路由结合后放到store里面,方便首页或者sidebar做菜单的显示隐藏
            permissionStore.M_ROUTES(accessRoutes)
            //成功拿到用户信息
            permissionStore.M_IS_GET_USER_INFO(true)

            // hack method to ensure that addRoutes is complete
            // set the replace: true, so the navigation will not leave a history record
            next({ ...to, replace: true })
          } catch (error) {
            // 如果出错了,把令牌去掉,并让用户重新去到登录页面
            console.log('error: ', error)
            await userStore.resetState()
            next(`/login?redirect=${to.path}`)
          }
        }
      }
    } else {
      //这里是没有令牌的情况
      //whiteList.indexOf(to.path) !== -1)判断用户请求的路由是否在白名单里
      if (whiteList.indexOf(to.path) !== -1) {
        // 不是-1就证明存在白名单里,不管你有没有令牌,都直接去到白名单路由对应的页面
        next()
      } else {
        // 如果这个页面不在白名单里,直接跳转到登录页面
        next(`/login?redirect=${to.path}`)
      }
    }
  })
}

src/store/permission.js

import { defineStore } from 'pinia'

import { staticRoutes, dynamicRoutes } from '@/router'

/**
 * Use meta.moduleName to determine if the current user has permission
 * @param module
 * @param route
 */
function hasPermission(module, route) {
  const title = route.meta?.title

  return title ? module.includes(title) : true
}

/**
 * Filter asynchronous routing tables by recursion
 * @param routes dynamicRoutes
 * @param module
 */
export function filterDynamicRoutes(routes, module) {
  const res = []

  routes.forEach(route => {
    const tmp = { ...route }

    if (hasPermission(module, tmp)) {
      if (tmp.children) {
        tmp.children = filterDynamicRoutes(tmp.children, module)
      }

      res.push(tmp)
    }
  })

  return res
}

export const usePermissionStore = defineStore('permission', {
  state: () => {
    return {
      isGetUserInfo: false, // get userInfo
      routes: [], //将过滤后的动态路由和静态路由集合
      dynamicRoutes: [], //过滤后的动态路由
    }
  },

  /***
   *封装处理数据的函数(业务逻辑):修改数据
   */
  actions: {
    M_ROUTES(routes) {
      this.$patch(state => {
        state.dynamicRoutes = routes
        state.routes = staticRoutes.concat(routes)
      })
    },
    M_IS_GET_USER_INFO(data) {
      this.isGetUserInfo = data
    },
    generateRoutes(module) {
      return filterDynamicRoutes(dynamicRoutes, module)
    },
  },
})

上文中提到的getToken()getInfo()就不展开讲了,

getToken() 方法,用于从cookie中获取到token

getInfo()方法,用户获取当前登录的用户信息,这里主要是需要拿到当前用户的权限(module)

  • 第三步: 在router/index.js文件中添加路由守卫 import { setBeforeEach } from './permission'
...

/* Permission Control */
import { setBeforeEach } from './permission'

export const staticRoutes = [
  ...
]
export const dynamicRoutes = [
    ...
]

const router = createRouter({
  // hash模式配置
  history: createWebHashHistory(),
  routes: staticRoutes,
})

/* 设置路由前置守卫 */
setBeforeEach(router)

export default router

总结

做好了以上三个步骤,其实就完成了最基本的权限管理。因为每个项目的需求不一样,接口返回的信息也可能不同,所以很多的细节没有写出来,但是思路基本上都是这一套。如果大家有更好的方案,欢迎一起学习和探讨!