(获取本节完整代码 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开发后台管理模板—方法指令、接口数据