五、vue+ElementUI开发后台管理模板—精确到按钮的权限控制

(获取本节完整代码 GitHub/chizijijiadami/vue-elementui-5

0、写在前面

管理后台该有的基本功能前面文章已经写完了,现在就写最后一个很重要的权限控制。

说到权限肯定是要有登录系统的,那就涉及到验证使用者的登录有效性问题,一般系统服务压力可控的项目是由后端直接进行 sessionid 操作来识别用户登录的,但若需要进行多业务系统整合,或者用户量庞大涉及分发服务器等情况时后端直接操作 sessionid 就显得捉襟见肘了,这时token机制就该上场了。

这篇文章主要内容包括:
● Token验证,登录 / 退出
● 页面+按钮权限控制

1、Token验证,登录 / 退出

这里从一个普通的SAP登录退出功能开始。
(1)添加接口和Mock数据
src>data>api>Login>index.js

import axiosApi from '@/common/utils/axiosApi'
import * as filter from './filter'
export function toLogin(params) {
    return axiosApi({
        url: '/toLogin',
        method: 'post',
        filter: filter.toLogin,
        params: params
    })
}

src>data>api>Login>filter.js

export const toLogin = {
  request(params) {
    return params
  },
  response(data) {
    return data
  }
}

拦截接口 src>data>mock>index.js

......
+ Mock.mock("/toLogin", "post", () => {
+   return {
+     status: 0,
+     data:{
+        token:"123"
+     },
+     message: "成功"
+   };
+ });

(2)新建登录状态记录
src>common>utils>index.js

const TokenKey = 'Admin-Token'
const err = 'Error:保存到本地存储失败!'
const errlimt = 'Error:本地存储超过限制!'

export function setStorage(key, value, exprise, type) {
  return new Promise(resolve => {
    // 默认7天过期(毫秒)
    let valueDate = JSON.stringify({
      value: value,
      time: new Date().getTime(),
      exprise: exprise || 60 * 60 * 24 * 7 * 1000,
      type: type || ''
    })
    try {
      window.localStorage.setItem(key || TokenKey, valueDate)
    } catch (e) {
      if (isQuotaExceeded(e)) {
        window.localStorage.clear()
        throw errlimt
      } else {
        throw err
      }
    }
    resolve()
  })
}

export function getStorage(key) {
  if (window.localStorage.getItem(key || TokenKey)) {
    let dataObj = JSON.parse(window.localStorage.getItem(key || TokenKey))
    let isTimed = new Date().getTime() - dataObj.time > dataObj.exprise
    if (isTimed) {
      window.localStorage.removeItem(key || TokenKey)
      return null
    } else {
      return dataObj.value
    }
  } else {
    return null
  }
}

// 非空判断
export function isNotEmpty(value) {
  return value !== undefined && value !== '' && value !== null
}

function isQuotaExceeded(e) {
  let flag = false
  if (e) {
    if (e.code) {
      switch (e.code) {
        case 22:
          flag = true
          break
        // fireFox
        case 1014:
          if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
            flag = true
          }
          break
      }
    } else if (e.number === -2147024882) {
      // ie
      flag = true
    }
  }
  return flag
}

(3)添加登录 / 退出 按钮
src>pages>Layout>Header.vue

<template>
  <div class="app-header">
    <Menu v-if="menuLocation==='H'" />
    <el-button
      v-if="menuLocation!=='H'"
      type="primary"
      plain
      @click="setMenuIsCollapse"
      :icon="isCollapse?'el-icon-s-fold':'el-icon-s-unfold'"
    ></el-button>
+    <el-button type="primary" v-if="!getStorage">
+      <router-link to="/login">登录</router-link>
+    </el-button>
+    <el-button type="primary" v-else @click="quit">退出</el-button>
  </div>
</template>

<script>
  import Menu from "./Menu";
  import { mapGetters } from "vuex";
+ import { getStorage, setStorage } from "common/utils";
export default {
  components: {
    Menu
  },
  computed: {
    ...mapGetters(["app"]),
    isCollapse() {
      return this.app.menu.isCollapse;
    },
    menuLocation() {
      return this.app.menu.location;
    },
+    getStorage() {
+      return getStorage();
+    }
  },
  methods: {
    setMenuIsCollapse() {
      this.$store.dispatch("setMenuIsCollapse");
    },
+    quit() {
+      setStorage().then(() => {
+         this.$router.push({
+           path: "/login?redirect="+this.$router.history.current.fullPath
+         });
+      });
+    }
  }
};
</script>

(4)新建登录页面

<template>
  <div class="app-login">
    <el-form ref="form" :model="form" label-width="80px">
      <el-form-item label="用户名" prop="name" :rules="regCheck({required:true,min:2,max:30})">
        <el-input v-model.trim="form.name" maxlength="30"></el-input>
      </el-form-item>
      <el-form-item label="密码" prop="pwd" :rules="regCheck({required:true,min:6})">
        <el-input v-model.trim="form.pwd" type="password" maxlength="20"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSubmit('form')">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
import * as api from "data/api/Login";
export default {
  name: "LoginIndex",
  data() {
    return {
      form: {
        name: "",
        pwd: ""
      }
    };
  },
  methods: {
    onSubmit(form) {
      this.$refs[form].validate(valid => {
        if (valid) {
          api.toLogin(form).then(data => {
            if (data.status === 0) {
            setStorage(null, data.data.token).then(() => {
                this.$router.push({
                  path: this.$route.query.redirect
                    ? this.$route.query.redirect
                    : "/"
                });
            }
          });
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    }
  }
};
</script>

<style lang="stylus" scoped>
.app-login
  width 500px
  margin 50px auto
</style>

(5)修改路由
src>router>index.js

  ......
+    {
+        path: '/login',
+        component: _import('Login/index')
+    },
     {
         path: '/404',
         component: _import('ErrorPages/404')
     },

(6)添加校验规则
src>common>validate>index.js

        if (_required) {
            rules.push({
                required: true,
                validator: validatorCode.checkNotNull.bind(item),
                trigger: _trigger
            })
        }
+        if (isNotEmpty(item.min) && (isNotEmpty(item.max) || isNotEmpty(item.maxLength))) {
+            rules.push({
+                min: item.min,
+                max: isNotEmpty(item.max) ? item.max : isNotEmpty(item.maxLength),
+                message: '字符长度在' + item.min + '至' + item.max + '之间!',
+                trigger: _trigger
+            })
+        } else if (isNotEmpty(item.min)) {
+            rules.push({
+                min: item.min,
+                message: '至少' + item.min + '个字符',
+                trigger: _trigger
+            })
+        } else if (isNotEmpty(item.max) || isNotEmpty(item.maxLength)) {
+            rules.push({
+                max: isNotEmpty(item.max) ? item.max : isNotEmpty(item.maxLength),
+                message: '至多' + item.max + '个字符',
+                trigger: _trigger
+            })
+        }
        if (_type) {

添加一个工具文件 src>common>utils>index.js

// 非空判断
export function isNotEmpty(value) {
    return value !== undefined && value !== '' && value !== null
  }

(7)修改全局路由守卫

+  import { getStorage } from '../utils'

......

router.beforeEach(async (to, from, next) => {
    document.title = getPageTitle(to.meta.title)

+    if (getStorage()) {
        if (store.getters.app.menu.list.length === 0) {
            store.dispatch("setMenuList", filterRouter(pagesRouterList))
            next({ ...to, replace: true })
        } else {
-             next()
+            if (to.path === '/login') {
                next('/')
+            } else {
+                next()
+            }
        }
+    } else {
+        if (to.path === '/login') {
+            next()
+        } else {
+            next('/login')
+        }
+    }
})

2、权限对接

(1)页面权限
a. 新建权限接口
src>data>api>Permission>index.js

import axiosApi from '@/common/utils/axiosApi'
import * as filter from './filter'
export function getPermission(params) {
    return axiosApi({
        url: '/permission',
        method: 'get',
        filter: filter.getPermission,
        params: params
    })
}

src>data>api>Permission>filter.js

export const getPermission = {
  request(params) {
    return params
  },
  response(data) {
    return data
  }
}

b. 新建权限接口Mock数据
每个路径页面都需要唯一标识符去识别,接口返回的 code 就是路由文件中的 name。
src>data>mock>permission>index.js

const Mock = require("mockjs");
Mock.mock("/permission", "get", () => {
    return {
      status: 0,
      data: {
        name: '测试',
        code: 'test',
        permission: {
          page: {
            code: 'pagePermission',
            name: "页面权限",
            children: [
              {
                code: 'Index',
                name: "首页",
                children: [
                  {
                    code: 'IndexIndex',
                    name: '首页',
                    children: [
                      {
                        code: 'IndexIndex_save',
                        name: '保存'
                      }
                    ]
                  }
                ]
              },
              {
                code:'List',
                name:'列表',
                children:[
                  {
                    code:'ListDetai',
                    name:'详情'
                  },
                  {
                    code:'ListFeature',
                    name:'特性'  
                  }
                ]
              }
            ]
          },
          api: {
            code: 'apiPermission',
            name: "接口权限",
            children: []
          }
        }
      },
      message: "成功"
    };
  });

c. 修改mock引入文件,src>data>mock>index.js

+  import './permission'

......

const Mock = require("mockjs");
// 使用mockjs模拟数据
let dataList = Mock.mock({
......

d. 修改路由定义
这里要将 pagesRouterList 中不需要权限控制的路由提取出来赋值给 constantRouterList,这个 constantRouterList 会进行初始化,而由权限控制的路由则会从接口获取后通过vue-router的方法 router-addroutes 动态添加到路由。

src>router>index.js

import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

const _import = file => () => import('@/pages/' + file + '.vue')
export const constantRouterList = [
    {
        path: '/login',
        component: _import('Login/index')
    },
    {
        path: '/404',
        component: _import('ErrorPages/404')
    },
    {
        path: '*',
        redirect: '/404'
    },
    {
        path: '',
        redirect: '/index/index'
    }
]
export const pagesRouterList = [
    {
        path: '/index',
        component: _import('Layout/index'),
        redirect: '/index/index',
        name: "Index",
        meta: {
            title: "首页",
            icon: "user",
            isShow: true
        },
        children: [
            {
                path: 'index',
                component: _import('Index/index'),
                name: "IndexIndex",
                meta: {
                    title: "首页",
                    icon: "user",
                    isShow: false
                }
            }
        ]
    },
    {
        path: '/list',
        component: _import('Layout/index'),
        name: "List",
        meta: {
            title: "列表",
            icon: "document",
            isShow: true
        },
        children: [
            {
                path: 'detail',
                component: _import('List/Detail/index'),
                name: "ListDetai",
                meta: {
                    title: "详情",
                    icon: "document",
                    isShow: true
                }
            },
            {
                path: 'feature',
                component: _import('List/Feature/index'),
                name: "ListFeature",
                meta: {
                    title: "特性",
                    icon: "document",
                    isShow: true
                }
            }
        ]
    }
]
export default new Router({
    scrollBehavior() {
        return { x: 0, y: 0 }
    },
    routes: constantRouterList
})

e. 添加权限状态值
修改 src>data>store>modules>app.js

const app = {
   state: {
       system: {
           title: "大米工厂",
       },
+        auth: {
+            page: [],
+            btn: []
+        },
       menu: {
           isCollapse: false,
           location: "V",   //V、VH、H三个值,V表示在左侧,VH表示横跨头部,H表示在头部
           list: [],
           obj: {}
       },
       tabs: {
           isShow: false
       },
       crumbs: {
           isShow: false
       },
       footer: {
           isShow: false
       }
   },
   mutations: {
       SET_MENU_ISCOLLAPSE: state => {
           state.menu.isCollapse = !state.menu.isCollapse
       },
       SETMENU_LIST: (state, menuList) => {
           state.menu.list = menuList
       },
+        SET_AUTH: (state, auth) => {
+            state.auth.page = auth.page
+            state.auth.btn = auth.btn
+        }
   },
   actions: {
       setMenuIsCollapse({ commit }) {
           commit('SET_MENU_ISCOLLAPSE')
       },
       setMenuList({ commit }, menuList) {
           commit('SETMENU_LIST', menuList)
       },
+        setAuth({ commit }, auth) {
+            commit('SET_AUTH', auth)
+        }
   }
}
export default app

f. 修改全局路由守卫
src>common>routerFilter>index.js

-  import { getStorage } from '../utils'
+  import { getStorage, setStorage } from '../utils'

......

router.beforeEach(async (to, from, next) => {
    document.title = getPageTitle(to.meta.title)

    if (getStorage()) {
        if (store.getters.app.menu.list.length === 0) {
-            store.dispatch("setMenuList", filterRouter(pagesRouterList))
-            next({ ...to, replace: true })
+            filterRouter(pagesRouterList).then(data => {
+                if (data) {
+                    store.dispatch("setAuth", data.auth)
+                    store.dispatch("setMenuList", data.menuList).then(() => {
+                        router.addRoutes(data.menuList)
+                        console.log(to.path);
+                        if (to.path === '/404') {
+                            next('/')
+                        } else {
+                            next({ ...to, replace: true })
+                        }
+                    })
+                } else {
+                    setStorage()
+                    next('/login')
+                }
+            })
        } else {
            if (to.path === '/login') {
                next('/')
            } else {
                next()
            }
        }
    } else {
        if (to.path === '/login') {
            next()
        } else {
            next('/login')
        }
    }
})

src>common>routerFilter>filter.js

  import { MessageBox } from 'element-ui'
  import store from 'store'
+ import * as api from 'data/api/Permission'
  export function filterRouter(pagesRouterList) {
-    let mennuList = pagesRouterList.filter(ele => ele.meta && ele.meta.isShow)
-    try {
-        if (mennuList.length <= 0) throw "没有可用菜单";
-        filterPage(mennuList)
-        return mennuList;
-    } catch (err) {
-        MessageBox({
-            message: err,
-            showCancelButton: false,
-            confirmButtonText: '确定',
-            type: 'error'
-        })
-    }
+      return api.getPermission().then(data => {
+          let auth = {
+              page: [],
+              btn: []
+          }
+          let permissionPage = data.data.permission.page;
+          try {
+              if (permissionPage.children && permissionPage.children.length <= 0) throw "您暂无权限请联系管理员";
+              authArrFilter(permissionPage, auth)
+              let authRouter = authRouterFilter(pagesRouterList, auth)
+              let menuList = authRouter;
+              filterPage(menuList)
+              return { auth: auth, menuList: menuList };
+          } catch (err) {
+              MessageBox({
+                  message: err,
+                  showCancelButton: false,
+                  confirmButtonText: '确定',
+                  type: 'error'
+              })
+          }
+      })
}

+  function authArrFilter(page, auth) {
+      if (page.children) {
+          page.children.forEach(function (item) {
+              if (item.code.match(/_/)) {
+                  auth.btn.push(item.code)
+              } else {
+                  auth.page.push(item.code)
+              }
+              authArrFilter(item, auth)
+          })
+      }
+  }

+  function authRouterFilter(pagesRouterList, auth) {
+      function _filter(list) {
+          return list.filter(item => {
+              if (item.children && item.children.length) {
+                  item.children = _filter(item.children)
+              }
+              return auth.page.includes(item.name)
+          })
+      }
+      return _filter(pagesRouterList)
+  }

function filterPage(menuList, pathFull, joinSign) {
    let pathFullCurrent = pathFull || ""
    let joinSignCurrent = joinSign || ""
    for (let i = 0; i < menuList.length; i++) {
        const ele = menuList[i];
        ele.pathFull = pathFullCurrent + joinSignCurrent + ele.path
        ele.showChildren = []
        store.getters.app.menu.obj[ele.name] = ele
        if (ele.children && ele.meta.isShow) {
            ele.showChildren = ele.children.filter(ele2 => ele2.meta.isShow)
            filterPage(ele.children, ele.pathFull, "/")
        }
    }
}

到这里页面权限就好了,运行修改 权限Mock数据,src>data>mock>permission>index.js

  page: {
            code: 'pagePermission',
            name: "页面权限",
            children: [

           ......

              {
                code:'List',
                name:'列表',
                children:[
-                  {
-                    code:'ListDetai',
-                    name:'详情'
-                  },
                  {
                    code:'ListFeature',
                    name:'特性'  
                  }
                ]
              }
            ]
          },

g. 运行如下图,详情菜单没有显示,如果直接输入 /list/detail 路径会发现跳转到404去了,因为没有权限。


菜单显示

(2)按钮权限
这里要用到 Vue-directive 的知识,前面已经有了按钮权限数组还是比较简单的。
● 新建全局指令文件
src>common>directives>index.js

import store from 'data/store'
export default {
  // 是否有按钮权限判定
  btnHas: {
    inserted(el, binding) {
      if (
        !store.getters.app.auth.btn.includes(binding.value)
      ) {
        if (!!window.ActiveXObject || 'ActiveXObject' in window) {
          el.parentNode.removeChild(el)
        } else {
          el.remove()
        }
      }
    }
  }
}

● main.js 引入

......

   //mockj数据
   import 'data/mock'
+  // 全局directive指令
+  import directives from './common/directives'
+  // 注册本页全局指令方法
+  Object.keys(directives).forEach(key => {
+    Vue.directive(key, directives[key])
+  })

new Vue({
  router,
  store,
  render: h => h(App),
}).$mount('#app')

● 页面使用
我们那下面图上的两个按钮试一下


修改 src>pages>Index>index.vue

......
-          <el-button type="primary"  @click="submitForm('form')">提交</el-button>
-          <el-button  @click="resetForm('form')">重置</el-button>
+          <el-button type="primary" v-btnHas="'IndexIndex_save'" @click="submitForm('form')">提交</el-button>
+          <el-button  v-btnHas="'IndexIndex_reset'" @click="resetForm('form')">重置</el-button>
.....

运行如下图,重置按钮因为不在权限接口中所以不显示了。



我们再修改Mock权限接口

......
{
      code: 'IndexIndex',
      name: '首页',
      children: [
         {
            code: 'IndexIndex_save',
            name: '保存'
         },
+         {
+            code: 'IndexIndex_reset',
+            name: '重置'
+          }
      ]
}
......

可以看到因为赋权了,重置按钮又出来了。


到这里完整的管理后台就写好了,我们后续见。

感谢阅读,喜欢的话点个赞吧:)
更多内容请关注后续文章。。。

一、vue入门基础开发—手把手教你用vue开发
二、vue+ElementUI开发后台管理模板—布局
三、vue+ElementUI开发后台管理模板—功能、资源、全局组件
四、vue+ElementUI开发后台管理模板—方法指令、接口数据

vue3 + vite + ElementPlus开发后台管理模板

vue实践1.1 企业官网——prerender-spa-plugin预渲染

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335