前言
最近在開發管理系統時遇到了任何管理系統都會有的需求---許可權控制,之前也遇到過這種需求,但是架構不完善導致的各種問題使得後期維護非常麻煩,這一次的方案解決了之前的種種問題,現做一次記錄,當然這個架構後期可能會有坑,不過得一步一步的嘗試才能發現並解決問題。
許可權控制需求
因為是單頁面應用,路由交給前端來控制,對於一些需要特定許可權才能檢視的資訊的保護變得尤為重要,如果前端不做好許可權校驗,後端也一時疏忽,就可能就會導致資料洩露。
對於許可權控制,需求大致為如下:
- 對於大模組的限制,比如需要通過路由跳轉的模組,這時需要進行路由攔截
- 對於小功能的限制,比如一個按鈕,如果沒有特定許可權,那麼這個按鈕就不顯示
安全層面的思考
之前接手了一個管理系統,前端是將許可權列表儲存在storage中來實現長久儲存,這種實現方式是很不可取的,因為hacker可以通過手動更改儲存的資訊來實現獲取特定許可權,甚至系統都沒有做路由攔截,如果知道模組的路由,可以直接通過輸入路由資訊來直接跳轉到特定模組。對於一些模組的許可權,許可權被管理員修改後也無法立即生效,所以對於這幾種情況做了如下思考與實踐。
許可權被管理員修改後立即生效
對於這個需求,我的做法是,獲取到許可權列表後,將許可權資訊儲存在 vuex store 中,並且使用getter函式,對於是否可以使用該許可權進行判斷,這樣一旦許可權資料更新,前端許可權限制功能點會自動修改,從而做到許可權的實時性,大致實現如下:
// vuex state.js
export default {
userPrivileges: {
admin: [],
purchaser: []
}, // 使用者許可權資訊
}
// vuex getters.js
export default {
canIUse: state => (role, id) => state.userPrivileges[role].includes(id)
}
// 頁面具體小功能,通過 mapGetters 引入 canIUse 函式
<span v-if="canIUse('admin', 9)">{{scope.row.allocation_subtotal}}</span>
複製程式碼
這樣一來,資料儲存在記憶體中,那麼許可權資訊就無法輕易的被修改,同時對於許可權的判斷也非常簡單,只需要在特定功能點傳入功能點的許可權id就能判斷是否可以使用這個許可權了。
但是將資料儲存在了記憶體中也會遇到一個問題,頁面重新整理怎麼辦?接下來就是講解這種情況。
重新整理頁面也可以進行許可權判斷
對於大模組的許可權攔截,肯定是通過路由鉤子來進行攔截的(這種實現有很多文章講解過,這裡不具體講解),但是通過路由鉤子進行攔截的前提是,許可權資訊得提前存在。
重新整理頁面會存在這種情況,頁面重新整理時,先執行的路由鉤子,再執行的元件生命週期鉤子來請求許可權的列表,此時許可權資訊不存在,那麼頁面跳轉到登陸頁的話,體驗就不夠友好。
所以我的做法是,建立一箇中間頁,如果許可權校驗不通過,那麼跳轉至中間頁,中間頁進行許可權的請求,請求到許可權後,再判斷是否可以跳轉,這樣的話,重新整理頁面體驗就比較好。大致程式碼如下:
// vuex actions.js
// 通過返回一個promise,使得store更新與後續程式碼變為“同步”執行
export default {
getUserPrivileges({ commit }) {
return fetch.get({
url: '/currentstaff'
}).then(data => {
commit('SET_USER_PRIVILEGES_INFO', data.data)
return data.data
}).catch(e => {
})
}
}
// router.js
// 需要驗證許可權的路由
{
path: 'suppliers',
component: Suppliers,
meta: {
role: 'admin',
privilegeId: 5
}
}
function isCanUseThisModule(to, from) {
return to.matched.every(record => {
// 利用路由meta儲存相應許可權資訊
if (record.meta.role) {
return store.getters.canIUse(record.meta.role, record.meta.privilegeId)
} else {
return true // 如果不需要許可權,直接返回true
}
})
}
router.beforeEach((to, from, next) => {
if (isCanUseThisModule(to, from)) {
next() // 許可權驗證通過,跳轉下一路由
} else {
next({
path: '/main/privilegeValidator' // 許可權驗證不通過時的中間頁
})
}
})
// 許可權校驗中間頁程式碼示例
created() {
this.$store.dispatch('getUserPrivileges').then(data => {
for (let i = 0; i < data.admin_permissions.length; i++) {
if (this.canIUse('admin', data.admin_permissions[i])) {
this.$router.push({
path: this.routerList.find(value => value.privilegeId === data.admin_permissions[i]).linkHref
})
return
}
}
this.$router.push('/login') // 如果沒有任何許可權,則跳轉登陸頁面
})
}
複製程式碼
使用者在登陸後也可以跳轉到這個許可權中間頁,進行許可權判斷後再跳轉到對應模組。
尾聲
大致的實現過程就是這樣,希望對大家有所幫助,如果有暗坑還請指出。