vuejs單頁應用的權限管理實踐

原文發佈於 http://blog.ahui.me/posts/2018-03-26/permission-control-of-vuejs/html

在衆多的B端應用中,簡單如小型企業的管理後臺,仍是大型的CMS,CRM系統,權限管理都是一個重中之重的需求,過往的web應用大多采起服務端模板+服務端路由的模式,權限管理天然也由服務端進行控制和過濾.可是在先後端分離的大潮下,若是採用單頁應用開發模式的話,前端也無可避免要配合服務端共同進行權限管理,接下來會以vuejs開發單頁應用爲例,給出一些嘗試方案,但願也能給你們提供一些思路.注意採用nodejs做爲中間層的先後端分離不在此文討論範圍.前端

目標

關於權限管理,因爲本人對服務端並不能算得上十分了解,我只能從我以往的項目經驗中進行總結,並不必定十分準確.vue

通常權限管理分爲如下幾部分.node

  • 應用使用權
  • 頁面級別權限
  • 模塊級別權限
  • 接口級別權限

接下來會逐一講解上述部分.完整的實例代碼託管在github-funkyLover/vue-permission-control-demo上.ios

應用使用權-登陸狀態管理與保存

首先應用使用權其實就是簡單的判斷登陸狀態而已.在不少C端應用,登陸以後能使用更多的功能在必定程度上也能夠算做權限管理的一部分.而在B端應用中通常表現爲不登陸則不能使用(固然還能使用相似找回密碼之類的功能).git

以往登陸狀態的保持通常經過session+cookie/token管理,用戶在打開網頁時就帶上cookie/token,由後端邏輯判斷並進行重定向.在SPA的模式下,頁面跳轉是由前端路由進行控制的,用戶狀態的判斷則須要由前端主動發送一次自動登陸的請求,根據返回結果進行跳轉.github

這個自動登陸的邏輯能夠深挖作出多種實現,例如登陸成功以後把用戶信息加密並經過localstorage在多個tab之間公用,這樣再新打開tab時就不須要再次自動登陸.這裏就以最簡單的實現來進行講解,基本流程以下:web

  1. 用戶請求頁面資源
  2. 檢查本地cookie/localstorage是否有token
  3. 若是沒有token,無論用戶請求打開的是哪一個路由,都一概跳轉到login路由
  4. 若是檢查到token,先請求自動登陸的接口,根據返回的結果判斷是進入用戶請求的路由仍是跳轉到login路由

而關於用戶狀態的判斷,通常應該針對進入login路由(包括忘記密碼之類的路由)和進入其餘路由進行判斷,在基於vuejs@2.x的前提下,能夠在router的beforeEach鉤子上進行用戶狀態判斷並切換路由便可.下面給出部分代碼:ajax

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並進行自動登陸vue-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)
      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的過期邏輯等.

頁面級別權限-根據權限生成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/路由懶加載也能夠實現對於沒有權限的路由不會加載相應頁面組件的資源.不過上述實現仍是有一些問題.

  1. 當頁面權限足夠細緻時,router的配置將會變得更加龐大難以維護
  2. 每當後臺更新頁面權限規則時,前端的判斷邏輯也要跟着改變,這就至關於先後端須要共同維護一套頁面級別權限.

第一個問題尚且能夠經過編碼手段來減輕,例如把邏輯放到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('/')
  }
}
複製代碼

這樣就實現了根據後端的返回動態擴展路由,固然也能夠根據後端的返回生成側欄或頂欄的導航菜單,這樣就不須要再在前端處理頁面權限了.這裏仍是要再提醒一下,本文的例子只實現最基本的功能,省略了不少可優化的邏輯

  1. 每打開新的tab(非login路由)時都會從新自動登陸並從新擴展router
  2. 每打開新的tab,自動登陸以後依然會跳轉到/路由,就算新打開的url爲/page1

解決思路是把用戶登陸信息和路由信息存儲在localstorage中,當打開新tab時直接經過localstorage中存儲的信息直接生成router對象.藉助store.jsvuex-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庫關聯不大,這裏簡單講一下如何處理.

  1. 首先從後端獲取容許當前用戶訪問的Api接口的權限
  2. 根據返回來的結果配置前端的ajax請求庫(如axios)的攔截器
  3. 在攔截器中判斷權限,根據需求提示用戶便可
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,若有問題或者意見請評論留言,我必虛心受教.

相關文章
相關標籤/搜索