vuejs單頁應用的許可權管理實踐

阿輝funkyLover發表於2018-03-28

原文釋出於 http://blog.ahui.me/posts/2018-03-26/permission-control-of-vuejs/

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

目標

關於許可權管理,由於本人對服務端並不能算得上十分了解,我只能從我以往的專案經驗中進行總結,並不一定十分準確.

一般許可權管理分為以下幾部分.

  • 應用使用權
  • 頁面級別許可權
  • 模組級別許可權
  • 介面級別許可權

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

應用使用權-登入狀態管理與儲存

首先應用使用權其實就是簡單的判斷登入狀態而已.在很多C端應用,登入之後能使用更多的功能在一定程度上也可以算作許可權管理的一部分.而在B端應用中一般表現為不登入則不能使用(當然還能使用類似找回密碼之類的功能).

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

這個自動登入的邏輯可以深挖做出多種實現,例如登入成功之後把使用者資訊加密並通過localstorage在多個tab之間公用,這樣再新開啟tab時就不需要再次自動登入.這裡就以最簡單的實現來進行講解,基本流程如下:

  1. 使用者請求頁面資源
  2. 檢查本地cookie/localstorage是否有token
  3. 如果沒有token,不管使用者請求開啟的是哪個路由,都一律跳轉到login路由
  4. 如果檢查到token,先請求自動登入的介面,根據返回的結果判斷是進入使用者請求的路由還是跳轉到login路由

而關於使用者狀態的判斷,一般應該針對進入login路由(包括忘記密碼之類的路由)和進入其他路由進行判斷,在基於vuejs@2.x的前提下,可以在router的beforeEach鉤子上進行使用者狀態判斷並切換路由即可.下面給出部分程式碼:

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並進行自動登入

// 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,如有問題或者意見請評論留言,我必虛心受教.

相關文章