Vue權(quán)限控制
1.權(quán)限相關(guān)概念
1.1.權(quán)限的分類
后端權(quán)限
前端權(quán)限
1.2.前端權(quán)限的意義
降低?法操作的可能性
盡可能排除不必要請(qǐng)求,減輕服務(wù)器壓?
提??戶體驗(yàn)
2.前端權(quán)限控制思路
2.1.菜單的控制
2.2.界?的控制
2.3.按鈕的控制
2.4.請(qǐng)求和響應(yīng)的控制
3. Vue的權(quán)限控制實(shí)現(xiàn)
3.1.菜單的控制
3.2.界?的控制
3.3.按鈕的控制
3.4.請(qǐng)求和響應(yīng)的控制
4.?結(jié)
4.1.菜單控制
4.2.界?控制
4.3.按鈕控制
4.4.請(qǐng)求和響應(yīng)控制
在Web系統(tǒng)中, 權(quán)限很久以來(lái)?直都只是后端程序所控制的,為什么呢? 因?yàn)閃eb系統(tǒng)的本質(zhì)圍繞的是數(shù)據(jù), ?和數(shù)據(jù)庫(kù)最緊密接觸的是后端程序。所以在很?的?段時(shí)間內(nèi),權(quán)限?直都只是后端程序需要考慮的話題。但是隨著前后端分離架構(gòu)的流?,越來(lái)越多的項(xiàng)?也在前端進(jìn)?權(quán)限控制。
1.權(quán)限相關(guān)概念
1.1.權(quán)限的分類
后端權(quán)限
從根本上講前端僅僅只是視圖層的展示, 權(quán)限的核?是在于服務(wù)器中的數(shù)據(jù)變化, 所以后端才是權(quán)限的關(guān)鍵, 后端權(quán)限可以控制某個(gè)?戶是否能夠查詢數(shù)據(jù), 是否能夠修改數(shù)據(jù)等操作
后端如何知道該請(qǐng)求是哪個(gè)?戶發(fā)過(guò)來(lái)的
cookie
session
token
后端的權(quán)限設(shè)計(jì)RBAC
?戶
??
權(quán)限
前端權(quán)限
前端權(quán)限的控制本質(zhì)上來(lái)說(shuō), 就是控制前端的 視圖層的展示和前端所發(fā)送的請(qǐng)求. 但是只有前端權(quán)限控制沒(méi)有后端權(quán)限控制是萬(wàn)萬(wàn)不可的. 前端權(quán)限控制只可以說(shuō)是達(dá)到錦上添花的效果.
1.2.前端權(quán)限的意義
如果僅從能夠修改服務(wù)器中數(shù)據(jù)庫(kù)中的數(shù)據(jù)層?上講,確實(shí)只在后端做控制就?夠了, 那為什么越來(lái)越多的項(xiàng)?也進(jìn)?了前端權(quán)限的控制, 主要有這???的好處
降低?法操作的可能性
不怕賊偷就怕賊惦記, 在??中展示出?個(gè) 就算點(diǎn)擊了也最終會(huì)失敗 的按鈕, 勢(shì)必會(huì)增加有?者?法操作的可能性
盡可能排除不必要請(qǐng)求,減輕服務(wù)器壓?
沒(méi)必要的請(qǐng)求, 操作失敗的請(qǐng)求, 不具備權(quán)限的請(qǐng)求, 應(yīng)該壓根就不需要發(fā)送, 請(qǐng)求少了, ?然也會(huì)減輕服務(wù)器的壓?
提??戶體驗(yàn)
根據(jù)?戶具備的權(quán)限為該?戶展現(xiàn)??權(quán)限范圍內(nèi)的內(nèi)容,避免在界?上給?戶帶來(lái)困擾, 讓?戶專注于分內(nèi)之事
2.前端權(quán)限控制思路
2.1.菜單的控制
在登錄請(qǐng)求中, 會(huì)得到權(quán)限數(shù)據(jù), 當(dāng)然, 這個(gè)需要后端返回?cái)?shù)據(jù)的?持. 前端根據(jù)權(quán)限數(shù)據(jù), 展示對(duì)應(yīng)的菜單.點(diǎn)擊菜單,才能查看相關(guān)的界?.
2.2.界?的控制
如果?戶沒(méi)有登錄,?動(dòng)在地址欄敲?管理界?的地址, 則需要跳轉(zhuǎn)到登錄界?如果?戶已經(jīng)登錄, 可是?動(dòng)敲??權(quán)限內(nèi)的地址, 則需要跳轉(zhuǎn)404界?
2.3.按鈕的控制
在某個(gè)菜單的界?中, 還得根據(jù)權(quán)限數(shù)據(jù), 展示出可進(jìn)?操作的按鈕, ?如刪除,修改,增加
2.4.請(qǐng)求和響應(yīng)的控制
如果?戶通過(guò)?常規(guī)操作, ?如通過(guò)瀏覽器調(diào)試?具將某些禁?的按鈕變成啟?狀態(tài), 此時(shí)發(fā)的請(qǐng)求, 也應(yīng)該被前端所攔截
3. Vue的權(quán)限控制實(shí)現(xiàn)
3.1.菜單的控制
查看登錄之后獲取到的數(shù)據(jù)
{
"data": {
"id": 500,
"rid": 0,
"username": "admin",
"mobile": "13999999999",
"email": "123999@qq.com",
"token": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjUwMCwicmlkIjowLCJpYXQiOjE1MTI1NDQyOTksImV4cCI6MTUxMjYzMDY5OX0.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
}
}
在這部分?jǐn)?shù)據(jù)中, 除了該?戶的基本信息之外, 還有兩個(gè)字段很關(guān)鍵
token,?于前端?戶的狀態(tài)保持
rights:該?戶具備的權(quán)限數(shù)據(jù),?級(jí)權(quán)限就對(duì)應(yīng)?級(jí)菜單,?級(jí)權(quán)限就對(duì)應(yīng)?級(jí)菜單
根據(jù)rights中的數(shù)據(jù), 動(dòng)態(tài)渲染左側(cè)菜單欄, 數(shù)據(jù)在Login.vue得到, 但是在Home.vue才使?, 所以可以把數(shù)據(jù)?vuex進(jìn)?維護(hù)
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
},
刷新界?菜單消失
原因分析
因?yàn)椴藛螖?shù)據(jù)是登錄之后才獲取到的, 獲取菜單數(shù)據(jù)之后,就存放在Vuex中
?旦刷新界?, Vuex中的數(shù)據(jù)會(huì)重新初始化, 所以會(huì)變成空的數(shù)組
因此, 需要將權(quán)限數(shù)據(jù)存儲(chǔ)在sessionStorage中, 并讓其和Vuex中的數(shù)據(jù)保持同步
代碼解決
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))
}
},
actions: {},
getters: {}
})
標(biāo)識(shí)?戶名, ?便查看當(dāng)前?戶
vuex的代碼
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的代碼
login() {
this.$refs.loginFormRef.validate(async valid => {
? ? ......
? ? this.$store.commit('setRightList', res.rights)
? ? this.$store.commit('setUsername', res.data.username)
? ? this.$message.success('登錄成功')
? ? this.$router.push('/home')
})
}
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()
},
3.2.界?的控制
1.正常的邏輯是通過(guò)登錄界?, 登錄成功之后跳轉(zhuǎn)到管理平臺(tái)界?, 但是如果?戶直接敲?管理平臺(tái)的地址, 也是可以跳過(guò)登錄的步驟.所以應(yīng)該在某個(gè)時(shí)機(jī)判斷?戶是否登錄
如何判斷是否登錄
sessionStorage.setItem('token', res.data.token)
什么時(shí)機(jī)
路由導(dǎo)航守衛(wèi)
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next()
} else {
const token = sessionStorage.getItem('token') if (!token) {
next('/login')
} else {
next()
}
}
})
2.雖然菜單項(xiàng)已經(jīng)被控制住了, 但是路由信息還是完整的存在于瀏覽器,正?如zhangsan這個(gè)?戶并不具備??這個(gè)菜單, 但是他如果??在地址欄中敲?/roles的地址, 依然也可以訪問(wèn)??界?
路由導(dǎo)航守衛(wèi)
路由導(dǎo)航守衛(wèi)固然可以在每次路由地址發(fā)?變化的時(shí)候, 從vuex中取出rightList判斷?戶將要訪問(wèn)的界?, 這個(gè)?戶到底有沒(méi)有權(quán)限.不過(guò)從另外?個(gè)?度來(lái)說(shuō),這個(gè)?戶不具備權(quán)限的路由, 是否也應(yīng)該壓根就不存在呢?
動(dòng)態(tài)路由
登錄成功之后動(dòng)態(tài)添加
App.vue中添加
代碼如下:
router.js
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
Login.vue
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()
}
}
3.3.按鈕的控制
按鈕控制
雖然?戶可以看到某些界?了, 但是這個(gè)界?的?些按鈕,該?戶可能是沒(méi)有權(quán)限的.因此, 我們需要對(duì)組件中的?些按鈕進(jìn)?控制. ?戶不具備權(quán)限的按鈕就隱藏或者禁?, ?在這塊中, 可以把該邏輯放到?定義指令中
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) {
// 不具備權(quán)限
const type = binding.value.effect
if (type === 'disabled') {
el.disabled = true el.classList.add('is-disabled')
} else {
el.parentNode.removeChild(el)
}
}
}
}
})
main.js
import './utils/permission.js'
router.js
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'}"
3.4.請(qǐng)求和響應(yīng)的控制
請(qǐng)求控制
除了登錄請(qǐng)求都得要帶上token, 這樣服務(wù)器才可以鑒別你的身份
axios.interceptors.request.use(function(req) {
const currentUrl = req.url
if (currentUrl !== 'login') {
req.headers.Authorization = sessionStorage.getItem('token')
}
return req
})
如果發(fā)出了?權(quán)限內(nèi)的請(qǐng)求, 應(yīng)該直接在前端訪問(wèn)內(nèi)組織,雖然這個(gè)請(qǐng)求發(fā)到服務(wù)器也會(huì)被拒絕
import axios from 'axios'
import Vue from 'vue'
import router from '../router'
// 配置請(qǐng)求的跟路徑, ?前?mock模擬數(shù)據(jù), 所以暫時(shí)把這?項(xiàng)注釋起來(lái)
// axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
const actionMapping = {
get: 'view',
post: 'add',
put: 'edit',
delete: 'delete'
}
axios.interceptors.request.use(function(req) {
const currentUrl = req.url
if (currentUrl !== 'login') {
req.headers.Authorization = sessionStorage.getItem('token')
// 當(dāng)前模塊中具備的權(quán)限
// 查看 get請(qǐng)求?
// 增加 post請(qǐng)求?
// 修改 put請(qǐng)求?
// 刪除 delete請(qǐng)求
const method = req.method
// 根據(jù)請(qǐng)求, 得到是哪種操作
const action = actionMapping[method]
// 判斷action是否存在當(dāng)前路由的權(quán)限中
const rights = router.currentRoute.meta
if (rights && rights.indexOf(action) == -1) {
// 沒(méi)有權(quán)限
alert('沒(méi)有權(quán)限') return Promise.reject(new Error('沒(méi)有權(quán)限'))
}
}
return req
}) axios.interceptors.response.use(function(res) {
return res
}) Vue.prototype.$http = axios
響應(yīng)控制
得到了服務(wù)器返回的狀態(tài)碼401, 代表token超時(shí)或者被篡改了, 此時(shí)應(yīng)該強(qiáng)制跳轉(zhuǎn)到登錄界?
axios.interceptors.response.use(function(res) {
if (res.data.meta.status === 401) {
router.push('/login') sessionStorage.clear() window.location.reload()
}
return res
})
4.?結(jié)
前端權(quán)限的實(shí)現(xiàn)必須要后端提供數(shù)據(jù)?持, 否則?法實(shí)現(xiàn).返回的權(quán)限數(shù)據(jù)的結(jié)構(gòu),前后端需要溝通協(xié)商, 怎樣的數(shù)據(jù)使?起來(lái)才最?便.
4.1.菜單控制
權(quán)限的數(shù)據(jù)需要在多組件之間共享, 因此采?vuex
防?刷新界?,權(quán)限數(shù)據(jù)丟失, 所以需要存儲(chǔ)在sessionStorage, 并且要保證兩者的同步
4.2.界?控制
路由的導(dǎo)航守衛(wèi)可以防?跳過(guò)登錄界?
動(dòng)態(tài)路由可以讓不具備權(quán)限的界?的路由規(guī)則壓根就不存在
4.3.按鈕控制
路由規(guī)則中可以增加路由元數(shù)據(jù)meta
通過(guò)路由對(duì)象可以得到當(dāng)前的路由規(guī)則,以及存儲(chǔ)在此規(guī)則中的meta數(shù)據(jù)
?定義指令可以很?便的實(shí)現(xiàn)按鈕控制
4.4.請(qǐng)求和響應(yīng)控制
請(qǐng)求攔截器和響應(yīng)攔截器的使?
請(qǐng)求?式的約定restful
————————————————
版權(quán)聲明:本文為CSDN博主「前端路啊」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/m0_62118859/article/details/124275448