.jpg)
# 权限相关概念
# 权限的分类
- 后端权限
从根本上讲前端仅仅只是视图层的展示, 权限的核⼼是在于服务器中的数据变化, 所以后端才是权限 的关键, 后端权限可以控制某个⽤户是否能够查询数据,修改数据等操作后端如何知道该请求是哪个⽤户发过来的
cookie
session
token后端的权限设计RBAC
用户
角色
权限 - 前端权限 前端权限的控制本质上来说, 就是控制前端的 视图层的展示和前端所发送的请求. 但是只有前端权 限控制没有后端权限控制是万万不可的. 前端权限控制只可以说是达到锦上添花的效果.
# 前端权限的意义
- 降低非法操作作的可能性 不怕贼偷就怕贼惦记, 在⻚⾯中展示出⼀个 就算点击了也最终会失败 的按钮, 势必会增加有⼼者 ⾮法操作的可能性
- 尽可能排除不必要请求,减轻服务器压⼒ 没必要的请求, 操作失败的请求, 不具备权限的请求, 应该压根就不需要发送, 请求少了, ⾃然也会减 轻服务器的压⼒
- 提⾼⽤户体验
# 前端权限控制思路
# 菜单的控制
在登录请求中,会得到权限数据,需要后端返回数据的支持,前端根据权限数据展示对应的菜单,点击菜单才能看到相关的界面
"data": {
"id": 500,
"rid": 0,
"username": "admin",
"mobile": "13999999999",
"email": "123999@qq.com",
"token": "Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1M
TI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.eGrsrvwHmtPsO9r_pxHIQ5i5L1kX9RX444uwnRGaIM"
},
"rights": [{
"id": 125,
"authName": "⽤户管理",
"icon": "icon-user",
"children": [{
"id": 110,
"authName": "⽤户列表",
"path": "users",
"rights": ["view", "edit", "add", "delete"]
}]
}, {
"id": 103,
"authName": "⻆⾊管理",
"icon": "icon-tijikongjian",
"children": [{
"id": 111,
"authName": "⻆⾊列表",
"path": "roles",
"rights": ["view", "edit", "add", "delete"]
}]
}, {
"id": 101,
"authName": "商品管理",
"icon": "icon-shangpin",
"children": [{
"id": 104,
"authName": "商品列表",
"path": "goods",
"rights": ["view", "edit", "add", "delete"]
}, {
"id": 121,
"authName": "商品分类",
"path": "categories",
"rights": ["view", "edit", "add", "delete"]
}]
}],
"meta": {
"msg": "登录成功",
"status": 200
} }
- token:用于前端用户状态的保持
- rights:该用户具备的权限数据,⼀级权限就对应⼀级菜单,⼆级权限就对应⼆级菜单
根据rights中的数据动态渲染左侧菜单栏, 数据在Login.vue得到, 但是在Home.vue才使⽤, 所以可 以把数据⽤vuex进⾏维护
export default new Vuex.Store({
state: {
rightList:[]
},
mutations: {
setRightList(state, data) {
state.rightList = data
},
actions: {
},
getters: {
}
})
Login.vue
login() {
this.$refs.loginFormRef.validate(async valid => {
......
this.$store.commit('setRightList', res.rights)
this.$message.success('登录成功')
this.$router.push('/home')
})
}
Home.vue
import { mapState } from 'vuex'
computed: {
...mapState(['rightList'])
}
created() {
this.activePath = window.sessionStorage.getItem('activePath')
this.menulist = this.rightList
},
刷新界⾯菜单消失
因为菜单数据是登录之后才获取到的, 获取菜单数据之后,就存放在Vuex中
⼀旦刷新界⾯, Vuex中的数据会重新初始化, 所以会变成空的数组
因此, 需要将权限数据存储在sessionStorage中, 并让其和Vuex中的数据保持同步
export default new Vuex.Store({
state: {
rightList:JSON.parse(sessionStorage.getItem('rightList')||'[]')
},
mutations: {
setRightList(state, data) {
state.rightList = data
sessionStorage.setItem('rightList',JSON.stringify(data))
}
}
标识⽤户名, ⽅便查看当前⽤户
export default new Vuex.Store({
state: {
rightList:JSON.parse(sessionStorage.getItem('rightList')||'[]'),
username: sessionStorage.getItem('username')
},
mutations: {
setRightList(state, data) {
state.rightList = data
sessionStorage.setItem('rightList',JSON.stringify(data))
},
setUsername(state, data) {
state.username = data
sessionStorage.setItem('username',data)
}
},
actions: {
},
getters: {
}
})
//Login.vue
this.$store.commit('setUsername', res.data.username)
//Home.vue
computed: {
...mapState(['rightList','username'])
}
<el-button type="info" @click="logout">{{username}}退出</el-button>
退出按钮的逻辑
logout() {
sessionStorage.clear()
this.$router.push('/login')
window.location.reload()
},
# 界面的控制
如果用户没有登录,手动在地址栏输入管理界面的地址,则需要跳转到登录界面
如果用户已经登录,可是手动敲入非权限内的地址,则需要跳转404界面
- 正常的逻辑是通过登录界⾯, 登录成功之后跳转到管理平台界⾯, 但是如果⽤户直接敲⼊管理平台的地
址, 也是可以跳过登录的步骤.所以应该在某个时机判断⽤户是否登录
sessionStorage.setItem('token', res.data.token)
router.beforeEach((to,from,next)=>
if (to.path === '/login') {
next()
} else {
const token = sessionStorage.getItem('token')
if(!token) {
next('/login')
} else {
next()
}
- 虽然菜单项已经被控制住了, 但是路由信息还是完整的存在于浏览器,正⽐如zhangsan这个⽤户并不具
备⻆⾊这个菜单, 但是他如果⾃⼰在地址栏中敲⼊/roles的地址, 依然也可以访问⻆⾊界⾯
路由导航守卫固然可以在每次路由地址发⽣变化的时候, 从vuex中取出rightList判断⽤户将要访问 的界⾯, 这个⽤户到底有没有权限.不过从另外⼀个⻆度来说,这个⽤户不具备权限的路由, 是否也应 该压根就不存在呢
动态路由
import Vue from 'vue'
import Router from 'vue-router'
import Login from '@/components/Login.vue'
import Home from '@/components/Home.vue'
import Welcome from '@/components/Welcome.vue'
import Users from '@/components/user/Users.vue'
import Roles from '@/components/role/Roles.vue'
import GoodsCate from '@/components/goods/GoodsCate.vue'
import GoodsList from '@/components/goods/GoodsList.vue'
import NotFound from '@/components/NotFound.vue'
import store from '@/store'
Vue.use(Router)
const userRule = { path: '/users', component: Users }
const roleRule = { path: '/roles', component: Roles }
const goodsRule = { path: '/goods', component: GoodsList }
const categoryRule = { path: '/categories', component: GoodsCate }
const ruleMapping = {
'users': userRule,
'roles': roleRule,
'goods': goodsRule,
'categories': categoryRule
}
const router = new Router({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home,
redirect: '/welcome',
children: [
{ path: '/welcome', component: Welcome },
// { path: '/users', component: Users },
// { path: '/roles', component: Roles },
// { path: '/goods', component: GoodsList },
// { path: '/categories', component: GoodsCate }
]
},
{
path: '*',
component: NotFound
}
]
})
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next()
} else {
const token = sessionStorage.getItem('token')
if(!token) {
next('/login')
} else {
next()
}
}
})
export function initDynamicRoutes() {
const currentRoutes = router.options.routes
const rightList = store.state.rightList
rightList.forEach(item => {
item.children.forEach(item => {
currentRoutes[2].children.push(ruleMapping[item.path])
})
})
router.addRoutes(currentRoutes) }
export default router
import { initDynamicRoutes } from '@/router.js'
login() {
this.$refs.loginFormRef.validate(async valid => {
if (!valid) return
const { data: res } = await this.$http.post('login',
this.loginForm)
if (res.meta.status !== 200) return
this.$message.error('登录失败!')
this.$store.commit('setRightList', res.rights)
this.$store.commit('setUsername', res.data.username)
sessionStorage.setItem('token', res.data.token)
initDynamicRoutes()
this.$message.success('登录成功')
this.$router.push('/home')
})
}
//App.vue
import { initDynamicRoutes } from '@/router.js'
export default {
name: 'app',
created() {
initDynamicRoutes()
}
}
# 按钮的控制
在某个菜单的界面中,还得根据巨献数据,展示出可进行操作的按钮,比如删除,修改,增加
虽然⽤户可以看到某些界⾯了, 但是这个界⾯的⼀些按钮,该⽤户可能是没有权限的.因此, 我们需要对组
件中的⼀些按钮进⾏控制. ⽤户不具备权限的按钮就隐藏或者禁⽤, ⽽在这块中, 可以把该逻辑放到⾃定
义指令中
- 路由规则中可以增加路由元数据meta
- 通过路由对象可以得到当前的路由规则,以及存储在此规则中的meta数据
- ⾃定义指令可以很⽅便的实现按钮控制
//permission.js
import Vue from 'vue'
import router from '@/router.js'
Vue.directive('permission', {
inserted: function(el, binding){
const action = binding.value.action
const currentRight = router.currentRoute.meta
if(currentRight) {
if(currentRight.indexOf(action) == -1) {
// 不具备权限
const type = binding.value.effect
if(type === 'disabled') {
el.disabled = true
el.classList.add('is-disabled')
} else {
el.parentNode.removeChild(el)
}
}
}
}
export function initDynamicRoutes() {
const currentRoutes = router.options.routes
const rightList = store.state.rightList
rightList.forEach(item => {
item.children.forEach(item => {
const itemRule = ruleMapping[item.path]
itemRule.meta = item.rights
currentRoutes[2].children.push(itemRule)
})
})
router.addRoutes(currentRoutes) }
v-permission="{action:'add'}"
v-permission="{action:'delete', effect:'disabled'}"
# 请求和响应的控制
如果用户通过非常规操作,比如通过浏览器调试工具将某些禁用的按钮变成启用转态,此时发的请求,也应该被前端所拦截
- 除了登录请求都得要带上token, 这样服务器才可以鉴别你的身份
axios.interceptors.request.use(function(req){
constcurrentUrl=req.urlif(currentUrl!=='login') {
req.headers.Authorization=sessionStorage.getItem('token')
}
return req
})
- 如果发出了⾮权限内的请求, 应该直接在前端访问内阻止,虽然这个请求发到服务器也会被拒绝
importaxiosfrom'axios'
importVuefrom'vue'
importrouterfrom'../router'// 配置请求的跟路径, ⽬前⽤mock模拟数据, 所以暂时把这⼀项注释起来// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
constactionMapping= {
get: 'view',post: 'add',put: 'edit',delete: 'delete'}
axios.interceptors.request.use(function(req){
constcurrentUrl=req.urlif(currentUrl!=='login') {
req.headers.Authorization=sessionStorage.getItem('token')// 当前模块中具备的权限// 查看 get请求// 增加 post请求// 修改 put请求// 删除 delete请求
const method=req.method// 根据请求, 得到是哪种操作
const action=actionMapping[method]// 判断action是否存在当前路由的权限中
const rights=router.currentRoute.meta
if(rights&&rights.indexOf(action) ==-1) {
// 没有权限alert('没有权限')
return Promise.reject(newError('没有权限'))
} }
return req
})
axios.interceptors.response.use(function(res){
return res})
Vue.prototype.$http=axios
得到了服务器返回的状态码401, 代表token超时或者被篡改了, 此时应该强制跳转到登录界⾯
axios.interceptors.response.use(function(res){
if (res.data.meta.status===401) {
router.push('/login')sessionStorage.clear()window.location.reload() }
return res })