- A+
1.1 权限的分类
-
后端权限
从根不上讲前端仅仅只是视图层的展示, 权限的核心是在于服务器中的数据变化,所以后端才是权限的关键,后端权限可以控制某个用户是否能够查询数据, 是否能够修改数据等操作。
1.后端如何知道该请求是哪个用户发过来的
cookie
session
token
2.后端的权限设计RBAC(后面再了解)
用户
角色
权限
-
前端权限
前端权限的控制本质上来说,就是控制端的视图层的展示和前端所发送的请求。但是只有前端权限控制没有后端权限控制是万万不可的。前端权限控制只可以说是达到锦上添花的效果。
1.2 前端权限的意义
如果仅从能够修改服务器中数据库中的数据层面上讲, 确实只在后端做控制就足够了, 那为什么越来越多的项目也进行了前端权限的控制, 主要有这几方面的好处。
- 降低非法操作的可能性
不怕赃偷就怕贼惦记, 在页面中展示出一个就算点击了也最终会失败的按钮,势必会增加有心者非法操作的可能性。 - 尽可能排除不必要清求, 减轻服务器压力
没必要的请求, 操作失败的清求, 不具备权限的清求, 应该压根就不需要发送, 请求少了, 自然也会减轻服务器的
压力。 - 提高用户体验
根据用户具备的权限为该用户展现自己权限范围内的内容, 避免在界面上给用户带来困扰, 让用户专注于分内之事。
二、前端权限的控制思路
2.1菜单(侧边栏)的控制
在登录请求中,会得到权限数据,当然,这个需要后端返回数据的支持(我们也可以自己模拟数据),前端根据权限数据,展示对应的菜单,点击菜单,才能查看到相关的界面。
2.2界面的控制
如果用户没有登录,手动在地址栏敲入管理界面的地址,则需要跳转到登录界面。
如果用户已经登录,如果手动敲入非权限内的地址,则跳转不过去或者需要跳转到404界面。
2.3按钮的控制
在某个菜单的界面中,还得根据权限数据,展示出可进行操作的按钮,比如删除,修改,增加。
2.4请求和响应的控制
如果用户通过非常规操作,比如通过浏览器调试工具将某些禁用的按钮变成启用状态,此时发的请求,也应该被前端所拦截。
三、实现步骤
3.1权限菜单栏控制
在src文件夹下新建一个mock文件夹,在mock文件夹下面新建一个js文件,然后在控制台输入npm install mockjs安装mockjs模块,之后在main.js中导入mock文件。在mock文件下写一个模拟数据,分配两个账户,分别有各自的菜单列表和token。
代码如下:
//使用Mock const Mock = require('mockjs') Mock.setup({ timeout: '500-1000' }) // 用户信息 const users = [ { id: 1, username: 'user', password: '123456', photo: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2667925509,1048662418&fm=11&gp=0.jpg', token: 'user-token', role: 'user', rights: [] }, { id: 2, username: 'admin', password: '123456', photo: 'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=2364244149,3298797080&fm=26&gp=0.jpg', token: 'admin-token', role: 'admin', rights: [] } ] // 权限信息 将权限信息从用户信息中抽离出来 不同身份对应不同的路由信息 // 这样方便了后期的维护 否则以后每加一个页面就需要在每个用户信息中做更改 十分的不方便 如果用户多了更是增添了不必要的麻烦 // 将用户直接分为不同身份 然后对不同身份做处理 这样比较合理 const roles = { user: [ { id: 1, authName: '评估管理', icon: 'el-icon-connection', children: [ { id: 11, authName: '估价师', icon: 'el-icon-s-grid', path: 'appraiser', rights: ['view'] }, { id: 12, authName: '估价师库', icon: 'el-icon-s-marketing', path: 'appraiserLibrary', rights: ['view'] } ] } ], admin: [ { id: 1, authName: '评估管理', icon: 'el-icon-connection', children: [ { id: 11, authName: '估价师', icon: 'el-icon-s-grid', path: 'appraiser', rights: ['view', 'edit', 'add', 'delete'] }, { id: 12, authName: '估价师库', icon: 'el-icon-s-marketing', path: 'appraiserLibrary', rights: ['view', 'edit', 'add', 'delete'] } ] }, { id: 2, authName: '项目管理', icon: 'el-icon-set-up', children: [ { id: 21, authName: '项目登记', icon: 'el-icon-s-custom', path: 'ProjectRegistration', rights: ['view', 'edit', 'add', 'delete'] }, { id: 22, authName: '项目查询', icon: 'el-icon-s-custom', path: 'ProjectQuery', rights: ['view', 'edit', 'add', 'delete'] } ] } ] } // 用户登录 Mock.mock('/login', 'post', option => { const { username, password } = JSON.parse(option.body) const user = users.find(item => { return item.username === username && item.password === password }) return user }) // 用户权限信息 Mock.mock('/roles', 'post', option => { return roles[option.body] }) js
-
然后我们把这个数据存放在vuex中,然后主页根据vuex中的数据进行菜单列表的渲染。
问题:刷新界面vuex数据消失,菜单栏消失。
解决:将数据存储在sessionStorage中,并让其和vuex中的数据保持同步。
vuex的代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
role: sessionStorage.getItem('role'),
rightList: JSON.parse(sessionStorage.getItem('rightList') || '[]'),//防止刷新侧边栏数据消失
username: sessionStorage.getItem('username'),
photo: sessionStorage.getItem('photo')
},
mutations: {
setRole (state, data) {
state.role = data
sessionStorage.setItem('role', data)
},
setRightList (state, data) {
state.rightList = data
sessionStorage.setItem('rightList', JSON.stringify(data))
},
setUsername (state, data) {
state.username = data
sessionStorage.setItem('username', data)
},
setPhoto (state, data) {
state.photo = data
sessionStorage.setItem('photo', data)
}
},
actions: {},
getters: {}
})
js
将mock里面的数据存入vuex后,我们需要在Login.vue的登录按钮事件中设置点击登录后将数据传入我们登录后的页面中。
此时我们在登录按钮的事件中这样写:
submitForm(form) {
this.$refs[form].validate((valid) => {
if (valid) {
this.loading = true
login(this.form).then(res => {
// console.log(res, 'login=>res')
// 将用户身份存入vuex 普通用户身份: user 管理员用户身份: admin
this.$store.commit('setRole', res.data.role)
this.$store.commit('setUsername', res.data.username)
this.$store.commit('setPhoto', res.data.photo)
sessionStorage.setItem('token', res.data.token)
getRoles(res.data.role).then(ret => {
// console.log(ret.data, 'getRoles=>ret.data')
// 将对应身份下的路由存储到vuex
this.$store.commit('setRightList', ret.data)
this.loading = false
this.$message.success('登陆成功')
// 根据用户所具备的权限 动态添加路由规则
initDynamicRoutes()
this.$router.push('/frame')
})
})
} else {
this.$message.error('用户名或密码错误');
return false;
}
});
},
js
传入框架页面之后,我们需要在frame(框架页面遍历侧边栏的菜单数据数组,使其显示对应用户的菜单列表),先导入相应的数据,
<script>
import { mapState } from 'vuex'
import {initDynamicRoutes} from "@/router";
export default {
js
computed: {
...mapState(['rightList']),//侧边栏数据
...mapState(['username']), // 用户名
...mapState(['photo']) // 用户头像
//三个可以写在一起!
},
created () {
// 初始化menulist菜单栏的数据
// this.menulist = this.rightList
initDynamicRoutes()
this.menuList = this.rightList
},
js
这样的话rightList数组数据就传给menuList,此时我们只需要将menuList遍历出来到侧边栏中就可以了。
<!-- 动态路由 -->
<!-- 一级菜单 -->
<el-submenu
:index="item.id+''"
v-for="item in menuList"
:key="item.index"
>
<template slot="title">
<span>{{item.authName}}</span>
</template>
<!--二级菜单-->
<el-menu-item
:index="'/'+subItem.path"
v-for="subItem in item.children"
:key="subItem.index"
>
<template >
<i :class="subItem.icon"></i>
<span>{{subItem.authName}}</span>
</template>
</el-menu-item>
</el-submenu>
html
3.2 界面的控制
登录成功后,将token数据存储在sessionStorage中,判断是否登录。
1.路由导航守卫
// 路由导航守卫
router.beforeEach((to, from, next) => {
if (to.path === '/Login') {
next()
} else {
const token = sessionStorage.getItem('token')
if (!token) {
next('/Login')
} else {
next()
}
}
})
js
问题:这样用户在登录之后就可以访问其他界面了,但如果用户A登录之后他只能访问a页面,他不能访问b页面,但是这时候他还是可以通过地址栏输入进入到b页面。
解决:当然我们也可以设置路由导航守卫,但是如果有多个页面,设置会非常不方便,并且对于用户A来说,她是不用访问b页面的,这时候我们干嘛不设置登录A用户时只显示A用户所要显示的路由呢,这时候我们用到动态路由。
2.动态路由
根据当前用户所拥有的权限数据动态添加所需要的路由。
1.先定义好所有的路由规则
//动态路由
const ProjectQueryRule={
path: '/ProjectQuery',
name:'ProjectQuery',
component:()=>import('@/views/projectManagement/ProjectQuery'),
}
const ProjectRegistrationRule={
path: '/ProjectRegistration',
name:'ProjectRegistration',
component:()=>import('@/views/projectManagement/ProjectRegistration'),
}
const appraiserRule={
path: '/appraiser',
name:'appraiser',
component:()=>import('@/views/evaluateManagement/appraiser'),
}
const appraiserLibraryRule={
path: '/appraiserLibrary',
name:'appraiserLibrary',
component:()=>import('@/views/evaluateManagement/appraiserLibrary'),
}
/**
* eslint报错
* error Unnecessarily quoted property 'xxx' found quote-props
* 表示 key没必要加引号 'table': tableRule => table: tableRule
*/
// 路由规则和字符串的映射关系
const ruleMapping = {
ProjectQuery: ProjectQueryRule,
ProjectRegistration: ProjectRegistrationRule,
appraiser: appraiserRule,
appraiserLibrary:appraiserLibraryRule
}
js
2.登录之后动态添加路由,注意这个initDynamicsRoutes的方法需要暴露出去在登录页面调用。
export function initDynamicRoutes () {
// console.log(router)
// 根据二级权限 对路由规则进行动态的添加
const currentRoutes = router.options.routes
//console.log(currentRoutes)
// currentRoutes[2].children.push()
const rightList = store.state.rightList
// console.log(rightList)
rightList.forEach(item => { // 如果是没有子路由的话 就直接添加进去 如果有子路由的话就进入二级权限遍历
// console.log(item, 'item-1')
if (item.path) {
const temp = ruleMapping[item.path]
// 路由规则中添加元数据meta
temp.meta = item.rights
currentRoutes[1].children.push(temp)
}
item.children.forEach(item => {
// item 二级权限
// console.log(item, 'item-2')
const temp = ruleMapping[item.path]
// 路由规则中添加元数据meta
temp.meta = item.rights
//console.log(temp.meta)
currentRoutes[2].children.push(temp)
})
})
console.log(currentRoutes)
router.addRoutes(currentRoutes)
}
js
这样当用户A在地址栏输入自己不能访问的路由时,则不会跳转到该页面,跳转到空白页面或者404页面。
**问题:**如果我们重新刷新的话动态路由就会消失,动态路由是在登录成功后才会调用的,刷新的时候并没有调用,所以动态路由没有添加上。
解决:可以在app.vue(框架页面)中created中调用添加动态路由的方法
created(){
initDynamicRoutes()
}
js
3.3按钮的控制
虽然用户可以看到对应的界面了,但是这个界面的一些按钮该用户可能没有权限。因此,我们需要对组件中的一些按钮进行控制,用户不具备权限的按钮就隐藏或者禁用,而在这块的实现中,可以把该逻辑放到自定义指令中。
这时我们需要根据我们mock设置的right属性来判断用户有什么权限。
添加自定义指令控制按钮
<!--如果添加估价师这个按钮有添加的权限,那就显示这个按钮,如果没有添加的权限,那就隐藏这个按钮。-->
<el-button type="primary" @click="addAppraiser()" v-permission="{action:'add'}">添加估价师</el-button>
<!--如果编辑这个按钮有添加的权限,那就显示这个按钮并且能点击,如果没有添加的权限,那就禁用这个按钮。-->
<el-button type="primary" size="mini" @click="handleEditBtn(row)" v-permission="{action:'add',effect:'disabled'}">编辑</el-button>
html
这样设置之后还不能实现效果,我们需要给这个设置对应的指令。
在util文件夹下新建permission.js文件,在里面设置我们这个自定义指令
// 自定义指令的注册
import Vue from 'vue'
import router from '@/router'
Vue.directive('permission', {
inserted (el, binding) {
// console.log(el)
// console.log(binding)
const action = binding.value.action
const effect = binding.value.effect
// 判断 当前的路由所对应的组件中 如何判断用户是否具备action的权限
console.log(router.currentRoute.meta, '按钮权限')
if (router.currentRoute.meta.indexOf(action) === -1) { // 等于-1说明没找到 不具备权限
if (effect === 'disabled') {
el.disabled = true
el.classList.add('is-disabled')
} else {
el.parentNode.removeChild(el)
}
}
}
})
js
这个按钮权限是通过meta属性传入的。
item.children.forEach(item => {
// item 二级权限
// console.log(item, 'item-2')
const temp = ruleMapping[item.path]
// 路由规则中添加元数据meta
temp.meta = item.rights
//console.log(temp.meta)
currentRoutes[2].children.push(temp)
})
js
设置完之后我们需要在main.js中导入import ‘./utils/permission.js’
3.4 请求和响应的控制
请求控制
- 除了登录请求都要带上token,这样服务器才可以鉴别我们的身份。
此时我们需要用到axios的请求拦截设置
在util文件夹上新建request.js文件,加入以下代码
// 请求拦截器
request.interceptors.request.use(req => {
// console.log(req.url)
// console.log(req.method)
if (req.url !== '/login' && req.url !== '/roles') {
// 不是登录的请求 也不是获取权限的请求 则在请求头中加入token 不知道如何使用Mock来验证请求头中的token 故此处注释
// req.headers.Authorization = sessionStorage.getItem('token')
const action = actionMapping[req.method]
// 判断非权限范围内的请求
// console.log(router)
const currentRight = router.currentRoute.meta
// console.log(currentRight)
if (currentRight && currentRight.indexOf(action) === -1) {
// 没有权限
alert('没有权限')
return Promise.reject(new Error('没有权限'))
}
}
return req
})
js
如果发出了非权限内的请求,应该直接在前端范围内阻止,虽然这个请求发到服务器也会被拒绝。
**非权限内的请求:**比如A用户是不能够操作该页面的按钮的,但是他通过f12调试把按钮改为可点击,如果我们不对这个请求进行处理,那么这个请求就会发送出去。
如果我们得到了服务器返回的状态码为401,代表token超时或者被篡改了,此时应该强制跳转到登录界面。request.js的完整代码如下。
import axios from 'axios'
import router from '@/router'
const request = axios.create()
// 映射
const actionMapping = {
get: 'view',
post: 'add',
put: 'edit',
delete: 'delete'
}
// request.defaults.baseURL = 'http://127.0.0.1:7001' // 注释掉之后调的接口将是Mock数据
// 请求拦截器
request.interceptors.request.use(req => {
// console.log(req.url)
// console.log(req.method)
if (req.url !== '/login' && req.url !== '/roles') {
// 不是登录的请求 也不是获取权限的请求 则在请求头中加入token 不知道如何使用Mock来验证请求头中的token 故此处注释
// req.headers.Authorization = sessionStorage.getItem('token')
const action = actionMapping[req.method]
// 判断非权限范围内的请求
// console.log(router)
const currentRight = router.currentRoute.meta
// console.log(currentRight)
if (currentRight && currentRight.indexOf(action) === -1) {
// 没有权限
alert('没有权限')
return Promise.reject(new Error('没有权限'))
}
}
return req
})
// 响应拦截器
request.interceptors.response.use(res => {
// console.log(res)
if (res.data.status === 401) {
router.push('/login')
sessionStorage.clear()
window.location.reload()
}
return res
})
export default request
js
同时也别忘了在main.js中导入。
四、小结
前端权限的实现之后需要后端提供数据支持,否则无法实现。
返回的权限数据的结构,前后端需要沟通协商怎样的数据用起来才最方便。
4.1 菜单的控制
权限的数据需要在多组件之间共享,因此采用Vuex
防止刷新界面,权限数据丢失,所以需要存在sessionStorage中,并且要保证两者的同步。
4.2 界面的控制
路由的导航守卫可以防止跳过登录界面
动态路由可以让不具备权限的路由规则压根就不存在
4.3 按钮控制
路由规则中可以增加路由元数据meta
通过路由对象可以得到当前的路由规则以及存在此规则中的meta数据
自定义指令可以很方便的实现按钮控制
4.4 请求和响应的控制
请求拦截器和响应拦截器的使用
- 我的微信
- 这是我的微信扫一扫
- 我的微信公众号
- 我的微信公众号扫一扫