原文發佈於 http://blog.ahui.me/posts/201...
在衆多的B端應用中,簡單如小型企業的管理後臺,仍是大型的CMS,CRM系統,權限管理都是一個重中之重的需求,過往的web應用大多采起服務端模板+服務端路由的模式,權限管理天然也由服務端進行控制和過濾.可是在先後端分離的大潮下,若是採用單頁應用開發模式的話,前端也無可避免要配合服務端共同進行權限管理,接下來會以vuejs開發單頁應用爲例,給出一些嘗試方案,但願也能給你們提供一些思路.注意採用nodejs做爲中間層的先後端分離不在此文討論範圍.html
關於權限管理,因爲本人對服務端並不能算得上十分了解,我只能從我以往的項目經驗中進行總結,並不必定十分準確.前端
通常權限管理分爲如下幾部分.vue
接下來會逐一講解上述部分.完整的實例代碼託管在github-funkyLover/vue-permission-control-demo上.node
首先應用使用權
其實就是簡單的判斷登陸狀態而已.在不少C端應用,登陸以後能使用更多的功能在必定程度上也能夠算做權限管理的一部分.而在B端應用中通常表現爲不登陸則不能使用(固然還能使用相似找回密碼之類的功能).ios
以往登陸狀態的保持通常經過session+cookie/token管理,用戶在打開網頁時就帶上cookie/token,由後端邏輯判斷並進行重定向.在SPA的模式下,頁面跳轉是由前端路由進行控制的,用戶狀態的判斷則須要由前端主動發送一次自動登陸
的請求,根據返回結果進行跳轉.git
這個自動登陸
的邏輯能夠深挖作出多種實現,例如登陸成功以後把用戶信息加密並經過localstorage在多個tab之間公用,這樣再新打開tab時就不須要再次自動登陸
.這裏就以最簡單的實現來進行講解,基本流程以下:github
自動登陸
的接口,根據返回的結果判斷是進入用戶請求的路由仍是跳轉到login路由而關於用戶狀態的判斷,通常應該針對進入login路由
(包括忘記密碼之類的路由)和進入其餘路由
進行判斷,在基於vuejs@2.x的前提下,能夠在router的beforeEach鉤子上進行用戶狀態判斷並切換路由便可.下面給出部分代碼:web
const routes = [ { path: '/', component: Layout, children: [ { path: '', name: 'Dashboard', component: Dashboard }, { path: 'page1', name: 'Page1', component: Page1 }, { path: 'page2', name: 'Page2', component: Page2 } ] }, { path: '/login', name: 'Login', component: Login } ] const router = new Router({ routes, mode: 'history' // 其餘配置 }) router.beforeEach((to, from, next) => { if (to.name === 'Login') { // 當進入路由爲login時,判斷是否已經登陸 if (store.getters.user.isLogin) { // 若是已經登陸,則進入功能頁面 return next('/') } else { return next() } } else { if (store.getters.user.isLogin) { return next() } else { // 若是沒有登陸,則進入login路由 return next('/login') } } })
在設定好跳轉邏輯後,咱們則須要在login路由中檢查是否有token並進行自動登陸ajax
// Login.vue async mounted () { var token = Cookie.get('vue-login-token') if (token) { var { data } = await axios.post('/api/loginByToken', { token: token }) if (data.ok) { this[LOGIN]() Cookie.set('vue-login-token', data.token) this.$router.push('/') } else { // 登陸失敗邏輯 } } }, methods: { ...mapMutations([ LOGIN ]), async login () { var { data } = await axios.post('/api/login', { username: this.username, password: this.password }) if (data.ok) { this[LOGIN]() Cookie.set('vue-login-token', data.token) this.$router.push('/') } else { // 登陸錯誤邏輯 } } }
同理退出登陸時把token置空便可.注意這裏給出的邏輯實現相對粗糙,實際應該根據需求進行改動,例如在進行自動登陸的時候給用戶適當的提示,把讀取/存儲token的邏輯放進store中進行統一管理,處理token的過期邏輯等.vue-router
這裏能夠藉助vue-router/路由獨享的守衛來進行處理.基本思路爲在每個須要檢查權限的路由中設置beforeEnter鉤子函數,並在其中對用戶的權限進行判斷.
const routes = [ { path: '/', component: Layout, children: [ { path: '', name: 'Dashboard', component: Dashboard }, { path: 'page1', name: 'Page1', component: Page1, beforeEnter: (to, from, next) => { // 這裏檢查權限並進行跳轉 next() } }, { path: 'page2', name: 'Page2', component: Page2, beforeEnter: (to, from, next) => { // 這裏檢查權限並進行跳轉 next() } } ] }, { path: '/login', name: 'Login', component: Login } ]
上面代碼是足以完成需求的,再配合上vue-router/路由懶加載也能夠實現對於沒有權限的路由不會加載相應頁面組件的資源.不過上述實現仍是有一些問題.
第一個問題尚且能夠經過編碼手段來減輕,例如把邏輯放到beforeEach鉤子中,又或者藉助高階函數對權限檢查邏輯進行抽象.可是第二個問題倒是無可避免的,若是咱們只在後端進行路由的配置,而前端根據後端返回的配置擴展router呢,這樣就能夠避免在先後端共同維護一套邏輯了,根據這個思路咱們對以前邏輯進行一下改寫.
// Login.vue async mounted () { var token = Cookie.get('vue-login-token') if (token) { var { data } = await axios.post('/api/loginByToken', { token: token }) if (data.ok) { this[LOGIN]() Cookie.set('vue-login-token', data.token) // 這裏調用更新router的方法 this.updateRouter(data.routes) } } }, // ... methods: { async updateRouter (routes) { // routes是後臺返回來的路由信息 const routers = [ { path: '/', component: Layout, children: [ { path: '', name: 'Dashboard', component: Dashboard } ] } ] routes.forEach(r => { routers[0].children.push({ name: r.name, path: r.path, component: () => routesMap[r.component] }) }) this.$router.addRoutes(routers) this.$router.push('/') } }
這樣就實現了根據後端的返回動態擴展路由,固然也能夠根據後端的返回生成側欄或頂欄的導航菜單,這樣就不須要再在前端處理頁面權限了.這裏仍是要再提醒一下,本文的例子只實現最基本的功能,省略了不少可優化的邏輯
自動登陸
並從新擴展router/
路由,就算新打開的url爲/page1
解決思路是把用戶登陸信息和路由信息存儲在localstorage中,當打開新tab時直接經過localstorage中存儲的信息直接生成router對象.藉助store.js和vuex-shared-mutations一類的插件能夠必定程度上簡化這部分邏輯,這裏不展開討論.
模塊級別的權限很好理解,其實就是帶權限判斷的組件.在React中藉助高階組件來定義須要過濾權限的組件是很是簡單且容易理解的.請看下面的例子
const withAuth = (Comp, auth) => { return class AuthComponent extends Component { constructor(props) { super(props); this.checkAuth = this.checkAuth.bind(this) } checkAuth () { const auths = this.props; return auths.indexOf(auth) !== -1; } render () { if (this.checkAuth()) { <Comp { ...this.props }/> } else { return null } } } }
上面的例子展現的就是有權限時展現該組件,沒有權限時則隱藏組件們能夠根據不一樣權限過濾需求來定義各類高階組件來處理.
而在vuejs中可使用經過render函數來實現
// Auth.vue import { mapGetters } from 'vuex' export default { name: 'Auth-Comp', render (h) { if (this.auths.indexOf(this.auth) !== -1) { return this.$slots.default } else { return null } }, props: { auth: String }, computed: { ...mapGetters(['auths']) } } // 使用 <Auth auth="canShowHello"> <Hello></Hello> </Auth>
vuejs中的render函數提供徹底編程的能力,甚至還能在render函數使用jsx語法,得到接近React的開發體驗,詳情參考vuejs文檔/渲染函數&jsx.
接口級別的權限通常就與UI庫關聯不大,這裏簡單講一下如何處理.
axios.interceptors.request.use((config) => { // 這裏進行權限判斷 if (/* 沒有權限 */) { return Promise.reject('no auth') } else { return config } }, err => { return Promise.reject(err) })
其實我的認爲前端也不必定有必要對請求的api進行權限判斷,畢竟接口不像路由,路由如今已經由前端來管理了,可是接口最終都須要經過服務器的校驗.能夠視需求加上.
寫得比較亂,像流水帳似的,完整的實例代碼在github-funkyLover/vue-permission-control-demo,若有問題或者意見請評論留言,我必虛心受教.