后台管理系统-页面路由权限控制篇

5,069 阅读5分钟

前言

对于一个后台管理系统来说权限控制是必须基本的,但是在此之前呢并没有太深入去了解,觉的市面上这么多轮子看到好看的好用的拿来用就行啦 ~~~ 。所以并不太了解它的实现方法和原理。

我查看了市面上一些比较守欢迎的后台系统。vue-element-admin Star:71.5k、 Ant Design Pro Star:30.4k(注意:这是React版本的,当然Ant Design也做了相应的Vue版本的)、vue-vben-admin Star: 7k(注意:这是一个Vue3+TS的后台系统项目而且也在一直更新当中)、iview-admin Star:15.9k。 这里就只例举4个吧,太多太好的系统模板了。

本篇文章就参考 vue-element-admin 的页面路由权限控制的实现方法,去实现一个后台管理系统的路由权限控制功能吧。

梳理vue-element-admin其实现

这里我就从vue-element-admin的登录页说起,一步一步走下去看看具体的实现流程。

登录流程:

handleLogin() -> this.$store.dispatch("user/login", this.loginForm) -> login()

整理下来登录的流程其实就二步:

  1. handleLogin() 校验处理用户填写的登录表单后执行Vuex中user模块的login方法

  2. login() 调相应的登录接口返回用户token并且存储在Cookies当中

登录成功后自然的就需要跳转到后台页中去,这里通过查看Network可以方法每一次登录都会去调一个接口 /vue-element-admin/user/info 一看就知道就是去获取自己的信息,这里有个最重要的就是roles

{
    "code":20000,
    "data":{
        "roles":[
            "admin"
        ],
        "introduction":"I am a super administrator",
        "avatar":"<https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif>",
        "name":"Super Admin"
    }
}

继续读其源码就可以发现最重要的部分了:VueRouter的全局路由前置守卫

const whiteList = ['/login', '/auth-redirect'] // no redirect whitelist

router.beforeEach(async(to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()

  if (hasToken) { 
    if (to.path === '/login') { 
      // if is logged in, redirect to the home page
      next({ path: '/' })
      NProgress.done() // hack: https://github.com/PanJiaChen/vue-element-admin/pull/2939
    } else { 
      // determine whether the user has obtained his permission roles through getInfo
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) { 
        next()
      } else { 
        try {
          // get user info
          // note: roles must be a object array! such as: ['admin'] or ,['developer','editor']
          const { roles } = await store.dispatch('user/getInfo')
          console.log('roles:',roles)
          // generate accessible routes map based on roles
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          console.log('accessRoutes:',accessRoutes)
          // dynamically add accessible routes
          router.addRoutes(accessRoutes)

          // 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) {
          // remove token and go to login page to re-login
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else { 
    /* has no token*/

    if (whiteList.indexOf(to.path) !== -1) {
      // in the free login whitelist, go directly
      next()
    } else {
      // other pages that do not have permission to access are redirected to the login page.
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

具体做了哪些事情呢?

  1. 如果没有token,查看 to.path 是否包含在 whiteList列表中

    1. 包含就放行
    2. 不包含就回登录页
  2. 如果有token,查看 to.path

    1. 如果去的是 /login 页 就 next('/')
    2. 如果去的不是 /login,就判断vuex中有没有roles,有角色就next,没有角色就要去获取用户信息,之后通过角色就去执行了 await store.dispatch('permission/generateRoutes', roles) 并且通过 router.addRoutes(accessRoutes) 挂载路由

而在generateRoutes这一步生成路由,只需要在前端去定义需要使用的路由和设定每个路由需要什么角色才能访问,在进行筛选,将当前角色可以访问的路由动态的挂载即可。

实现路由权限控制

接下来就参照上面的流程来搭建的后台管理系统实现简单的页面路由权限控制以及路由表的渲染

这里我使用的Vue3(setup的语法糖)+ TS 配合 Ant Design Vue 框架 采用 Koa2 + Mockjs来模拟数据。 项目源码

用户登录:

// 提交表单 且 前端表单数据校验成功后回调事件
const handleFinish = async (values: ILoginForm) => {
    spinSpinning.value = true
    const loginInfo = await store.dispatch('asyncHandleUserLogin', values)
    if (loginInfo === 'success') {
      setTimeout(() => {
        spinSpinning.value = false
        router.replace({ path: '/mine/index' })
      }, 1000)
    } else {
      message.warning({
        content: '登录异常,请稍后重试',
        onClose: () => {
          spinSpinning.value = false
        }
      })
    }
}

store/module/user.ts

asyncRouters异步路由存储在vuex当中

import { ILoginForm } from '../../types/index'
import { handleUserLogin } from '../../apis/user'

export default {
  state: () => {
    return {
      'access-token': '',
      userInfo: {},
      asyncRouters: []
    }
  },
  mutations: {
    handleUserLogin(state: any, accessToken: string) {
      state['access-token'] = accessToken

      localStorage.setItem('access-token', accessToken)
    },
    handleGetUserInfo(state: any, userInfo: object) {
      state.userInfo = userInfo
    },
    handleRouters(state: any, routers: object[]) {
      state.asyncRouters = routers
    }
  },
  actions: {
    async asyncHandleUserLogin(context: any, loginFrom: ILoginForm) {
      const loginRes: any = await handleUserLogin(loginFrom)
      if (loginRes.status === 200 && loginRes.msg === '登录成功') {
        await context.commit('handleUserLogin', loginRes.data.accessToken)
        return 'success'
      }
      return 'error'
    },
    async asyncHandleGetUserInfo(context: any, userInfo: object) {
      await context.commit('handleGetUserInfo', userInfo)
    },
    async asyncHandleRouters(context: any, routers: object[]) {
      await context.commit('handleRouters', routers)
    }
  },
  getters: {}
}

全局路由前置守卫以及如何生成路由

/* eslint-disable no-unused-vars */
import { RouteRecordRaw } from 'vue-router'
import { handleGetUserInfo } from '../apis/user'
import store from '../store'
import router, { asyncRouters } from './index'

const noTokenPassPaths: string[] = ['/login'] // 不需要token可以进的页面

/**
 * 判断角色
 * @param roles
 * @param route
 * @returns
 */
const hasRole: (roles: string[], route: RouteRecordRaw) => boolean = (roles, route) => {
  if (route.meta && route.meta.roles) {
    return roles.some(role => route.meta.roles.includes(role))
  }
  return true
}

/**
 * 过滤路由
 * @param roles
 * @param asyncRouters
 * @returns
 */
const filterRouters = async (roles: string[], asyncRouters: RouteRecordRaw[]) => {
  const res: RouteRecordRaw[] = []
  asyncRouters.forEach(async route => {
    if (route.children) {
      if (hasRole(roles, route)) {
        // eslint-disable-next-line no-param-reassign
        route.children = await filterRouters(roles, route.children)
      }
      if (route.children.length !== 0) {
        res.push(route)
      }
    } else if (hasRole(roles, route)) {
      res.push(route)
    }
  })
  return res
}

/**
 * 先根据角色判断是否需要进行过滤路由(admin管理员不需要过滤)
 * @param roles
 * @param asyncRouters
 */
const generateRouters = (roles: string[], asyncRouters: RouteRecordRaw[]) => {
  if (roles.includes('admin')) {
    return asyncRouters
  }
  return filterRouters(roles, asyncRouters)
}

/**
 * 异步处理路由
 * @param roles
 * @param asyncRouters
 */
const asyncHandleRoutrs = async (roles: string[], asyncRouters: RouteRecordRaw[]) => {
  const handleRouters = await generateRouters(roles, asyncRouters)
  handleRouters.forEach(async route => {
    router.addRoute(route)
  })
  await store.dispatch('asyncHandleRouters', handleRouters)
}

/**
 * 路由前置守卫
 * 有token执行的
 *    1. 有token,并且是从登陆页面来的->根据token获取用户基本信息(包含用户角色,权限)-> 生成对应角色的动态路由
 *    2. 有tokenm, 判断vuex是否有用户的信息有(生成对应角色的动态路由),没有(根据token获取用户基本信息(包含用户角色,权限)->生成对应角色的动态路由)
 * 没有token执行的
 *    1. 如果 to.path 去的路径没有包含在 noTokenPassPaths中则跳/login
 *    2. 有包含在noTokenPassPaths则next()
 */
router.beforeEach(async (to, from, next) => {
  const hasAccessToken: string = localStorage.getItem('access-token') || ''
  if (hasAccessToken) {
    let userInfo: any = store.state.userModule.userInfo || {}
    if (JSON.stringify(userInfo) === '{}') {
      const { data } = await handleGetUserInfo()
      userInfo = data
      await store.dispatch('asyncHandleGetUserInfo', userInfo)
      await asyncHandleRoutrs(userInfo.roles, asyncRouters)
      next({ ...to, replace: true })
    }
    next()
  } else if (noTokenPassPaths.indexOf(to.path) !== -1) {
    next()
  } else {
    next({ path: '/login' })
  }
})

所有的异步路由定义在router/index.ts当中

/**
 * 异步路由
 * 1. 个人中心页面    均可
 * 2. 权限测试页面
 *      页面权限管理    admin
 *      角色管理        admin editor
 * 3. 嵌套路由测试页面
 *      嵌套路由一       admin
 *      嵌套路由二       editor
 *      嵌套路由三       均可
 *          嵌套路由三-1  均可
 *          嵌套路由三-2  均可
 */
const asyncRouters: Array<RouteRecordRaw> = [
  {
    path: '/mine',
    name: 'Mine',
    component: Layout,
    children: [
      {
        path: 'index',
        name: 'MineIndex',
        component: import('@/views/my/mine.vue'),
        meta: { title: '个人中心页面', path: '/mine/index' }
      }
    ]
  },
  {
    path: '/permissionTest',
    name: 'PermissionTest',
    component: Layout,
    meta: { title: '权限测试页面' },
    children: [
      {
        path: 'page',
        name: 'PermissionTest-Page',
        component: import('@/views/permissiontest/permission.vue'),
        meta: { title: '页面权限管理页面', path: '/permissionTest/page', roles: ['admin'] }
      },
      {
        path: 'role',
        name: 'PermissionTest-Role',
        component: import('@/views/permissiontest/role.vue'),
        meta: { title: '角色管理页面', path: '/permissionTest/role', roles: ['admin', 'editor'] }
      }
    ]
  },
  {
    path: '/nestTest',
    name: 'nestTest',
    component: Layout,
    meta: { title: '嵌套路由测试页面' },
    children: [
      {
        path: 'test1',
        name: 'NestTest-test1',
        component: import('@/views/nestTest/test1.vue'),
        meta: { title: '嵌套路由一页面', path: '/nestTest/test1', roles: ['admin'] }
      },
      {
        path: 'test2',
        name: 'NestTest-test2',
        component: import('@/views/nestTest/test2.vue'),
        meta: { title: '嵌套路由二页面', path: '/nestTest/test2', roles: ['editor'] }
      },
      {
        path: 'test3',
        name: 'NestTest-test3',
        component: import('@/views/nestTest/test3/test3.vue'),
        meta: { title: '嵌套路由三页面' },
        children: [
          {
            path: 'test1',
            name: 'NestTest-test3-test1',
            component: import('@/views/nestTest/test3/test3-1.vue'),
            meta: { title: '一页面', path: '/nestTest/test3/test1' }
          },
          {
            path: 'test2',
            name: 'NestTest-test3-test2',
            component: import('@/views/nestTest/test3/test3-2.vue'),
            meta: { title: '二页面', path: '/nestTest/test3/test2' }
          }
        ]
      }
    ]
  }
]

这是页面路由权限控制的主要内容,可以唯一需要注意的是在vue-router4版本当中动态添加路由的API已经改为了router.addRoute(route)

效果

admin角色的用户看到的效果

image.png

editor角色的用户看到的效果

image.png

tourist角色的用户或说是没有任何权限的用户看到的效果

image.png

总结下来:实现页面路由权限控制整体的思想还是比较清晰的,但是在中途还是有很多坑得踩的。