vue后台登录权限管理实现思路

前言

后台项目区别于做其它的项目,权限验证与安全性是非常重要的,可以说是一个后台项目一开始就必须考虑和搭建的基础核心功能。我们所要做到的是:不同的权限对应着不同的路由,同时侧边栏也需根据不同的权限,异步生成。这里先简单说一下,我实现登录和权限验证的思路。
登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token,拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。
权限验证:通过token获取用户对应的 role,动态根据用户的 role 算出其对应有权限的路由,通过 router.addRoutes 动态挂载这些路由。
上述所有的数据和操作都是通过vuex全局管理控制的。(补充说明:刷新页面后 vuex的内容也会丢失,所以需要重复上述的那些操作)接下来,我们一起手摸手一步一步实现这个系统。

登录篇

绘制两个input输入框,在前端预校验后发送给服务器端。登录成功后,服务器端返回一个token(该token是一个能唯一标示用户身份的一个key),将token储存在cookie中。之后我们打开或者刷新页面的时候就不用再重新登录了。
(为了保证安全,token的有效期应设置为Session,在用户关闭浏览器时销毁。后端也会在每周固定一个时间点重新刷新token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账号。)

获取用户信息

用户登录成功之后,我们会在全局钩子router.beforeEach中拦截路由,页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有token,就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。

权限篇

权限控制主体思路

前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过router.addRoutes动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是绝对安全的,后端的权限验证是逃不掉的。
不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每一个后台的请求不管是 get 还是 post 都会让前端在请求 header里面携带用户的 token,后端会根据该 token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状态码,做出相对应的操作。

addRoutes

在之前通过后端动态返回前端路由一直很难做的,因为vue-router必须是要vue在实例化之前就挂载上去的,不太方便动态改变。不过好在vue2.2.0以后新增了router.addRoutes。有了这个我们就可相对方便的做权限控制了。

具体实现

  1. 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
  2. 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
  3. 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
  4. 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。

router.js

// router.js
import Vue from 'vue';
import Router from 'vue-router';

import Login from '../views/login/';
const dashboard = resolve => require(['../views/dashboard/index'], resolve);
//使用了vue-routerd的[Lazy Loading Routes
](https://router.vuejs.org/en/advanced/lazy-loading.html)

//所有权限通用路由表 
//如首页和登录页和一些不用权限的公用页面
export const constantRouterMap = [
  { path: '/login', component: Login },
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    name: '首页',
    children: [{ path: 'dashboard', component: dashboard }]
  },
]

//实例化vue的时候只挂载constantRouter
export default new Router({
  routes: constantRouterMap
});

//异步挂载的路由
//动态需要根据权限加载的路由表 
export const asyncRouterMap = [
  {
    path: '/permission',
    component: Layout,
    name: '权限测试',
    meta: { role: ['admin','super_editor'] }, //页面需要的权限
    children: [
    { 
      path: 'index',
      component: Permission,
      name: '权限测试页',
      meta: { role: ['admin','super_editor'] }  //页面需要的权限
    }]
  },
  { path: '*', redirect: '/404', hidden: true }
];

这里我们根据 vue-router官方推荐 的方法通过meta标签来标示改页面能访问的权限有哪些。如meta: { role: ['admin','super_editor'] }表示该页面只有admin和超级编辑才能有资格进入。
注意事项:这里有一个需要非常注意的地方就是 404页面一定要最后加载,如果放在constantRouterMap一同声明了404,后面的所以页面都会被拦截到404

main.js

关键的main.js

// main.js
router.beforeEach((to, from, next) => {
  if (store.getters.token) { // 判断是否有token
    if (to.path === '/login') {
      next({ path: '/' });
    } else {
      if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
        store.dispatch('GetInfo').then(res => { // 拉取info
          const roles = res.data.role;
          store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表
            router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表
            next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
          })
        }).catch(err => {
          console.log(err);
        });
      } else {
        next() //当有用户权限的时候,说明所有可访问路由已生成 如访问没权限的全面会自动进入404页面
      }
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
      next();
    } else {
      next('/login'); // 否则全部重定向到登录页
    }
  }
});

main.js的详细注释

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') {
      // 如果已经登录则重定向到首页
      next({ path: '/' })
      NProgress.done()
    } else {
      // 判断当前用户是否已获取到 user_info 信息
      const hasRoles = store.getters.roles && store.getters.roles.length > 0
      if (hasRoles) {
        next()
      } else {
        // 未获取到 user_info 信息
        try {
          // 获取 user_info
          // 权限列表必须是一个数组 例如: ['admin'] 或者 ['developer','editor']
          const { roles } = await store.dispatch('user/getInfo')
          // store.dispatch('user/getInfo') 代码如下:
          getInfo({ commit, state }) {
            return new Promise((resolve, reject) => {
              getInfo(state.token).then(response => {
                // getInfo 代码如下:
                // 携带 token 发送 get 请求来获取 user_info
                function getInfo(token) {
                  return request({
                    url: '/vue-element-admin/user/info',
                    method: 'get',
                    params: { token }
                  })
                }
                // getInfo 代码结束

                const { data } = response
                if (!data) {
                  // 如果data不存在则报错
                  reject('Verification failed, please Login again.')
                }
                // 解构出data中的 roles, name, avatar, introduction
                const { roles, name, avatar, introduction } = data
                // 权限列表必须是一个非空数组
                if (!roles || roles.length <= 0) {
                  // 如果为空数组则报错
                  reject('getInfo: roles must be a non-null array!')
                }
                // 将获取到的user_info存在state中
                commit('SET_ROLES', roles)
                commit('SET_NAME', name)
                commit('SET_AVATAR', avatar)
                commit('SET_INTRODUCTION', introduction)
                // 上述4行代码如下:
                SET_ROLES: (state, roles) => {
                  state.roles = roles
                }
                SET_NAME: (state, name) => {
                  state.name = name
                },
                  SET_AVATAR: (state, avatar) => {
                    state.avatar = avatar
                  },
                    SET_INTRODUCTION: (state, introduction) => {
                      state.introduction = introduction
                    }
                // 4行代码结束

                resolve(data)
              }).catch(error => {
                reject(error)
              })
            })
          }
          //生成当前角色可访问的路由表
          const accessRoutes = await store.dispatch('permission/generateRoutes', roles)
          // store.dispatch('permission/generateRoutes', roles)代码如下:
          generateRoutes({ commit }, roles) {
            return new Promise(resolve => {
              let accessedRoutes
              if (roles.includes('admin')) {
                // 如果roles包含admin,则accessedRoutes等于asyncRoutes
                // asyncRoutes为router中定义的所有需要权限的路由表
                // 也就是说admin拥有访问所有页面的权限
                accessedRoutes = asyncRoutes || []
              } else {
                //过滤出拥有roles权限的路由
                accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
                // filterAsyncRoutes代码如下:
                function filterAsyncRoutes(routes, roles) {
                  const res = []
                  //遍历需要权限的路由表
                  routes.forEach(route => {
                    const tmp = { ...route }
                    if (hasPermission(roles, tmp)) {
                      // hasPermission 代码如下:
                      function hasPermission(roles, route) {
                        if (route.meta && route.meta.roles) {
                          // 如果route中有roles权限则返回true,都没有则返回false
                          return roles.some(role => route.meta.roles.includes(role))
                        } else {
                          return true
                        }
                      }
                      // hasPermission 代码结束

                      //如果tmp中存在子路由
                      if (tmp.children) {
                        //递归 为tmp添加children属性(相当于添加子路由)
                        //属性值为含有该roles权限的路由
                        tmp.children = filterAsyncRoutes(tmp.children, roles)
                      }
                      // 将含有roles权限的路由添加到res中
                      res.push(tmp)
                    }
                  })
                  // 返回所有含有roles权限的路由表
                  // accessedRoutes = res
                  return res
                }
                // filterAsyncRoutes代码结束

              }
              commit('SET_ROUTES', accessedRoutes)
              // SET_ROUTES 代码如下:
              SET_ROUTES: (state, routes) => {
                state.addRoutes = routes
                // 将不需要权限的路由和需要权限的路由拼接到一起
                // constantRoutes为router中不需要权限的路由表
                state.routes = constantRoutes.concat(routes)
              }
              // SET_ROUTES 代码结束

              resolve(accessedRoutes)
            })
          }
          // generateRoutes 代码结束

          //动态添加可访问路由表
          router.addRoutes(accessRoutes)

          // hack方法 确保addRoutes已完成 设置replace: true 所以导航不会留下历史记录
          next({ ...to, replace: true })
        } catch (error) {
          // 删除token回到登录页面重新登录
          await store.dispatch('user/resetToken')
          Message.error(error || 'Has Error')
          next(`/login?redirect=${to.path}`)
          NProgress.done()
        }
      }
    }
  } else {
    /* 没有 token*/
    if (whiteList.indexOf(to.path) !== -1) {
      // 在免登录白名单中继续跳转
      next()
    } else {
      // 否则全部重定向到登录页
      next(`/login?redirect=${to.path}`)
      NProgress.done()
    }
  }
})

康雀西威自: https://juejin.im/post/591aa14f570c35006961acac
作者:花裤衩
来源:掘金

  • 8
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值