1. 程式人生 > 程式設計 >淺談vue許可權管理實現及流程

淺談vue許可權管理實現及流程

一、整體思路

後端返回使用者許可權,前端根據使用者許可權處理得到左側選單;所有路由在前端定義好,根據後端返回的使用者許可權篩選出需要掛載的路由,然後使用 addRoutes 動態掛載路由。

二、實現要點

(1)路由定義,分為初始路由和動態路由,一般來說初始路由只有 login,其他路由都掛載在 home 路由之下需要動態掛載。
(2)使用者登入,登入成功之後得到 token,儲存在 sessionStorage,跳轉到 home,此時會進入路由攔截根據 token 獲取使用者許可權列表。
(3)全域性路由攔截,根據當前使用者有沒有 token 和 許可權列表進行相應的判斷和跳轉,當沒有 token 時跳到 login,當有 token 而沒有許可權列表時去發請求獲取許可權等等邏輯。

(4)處理使用者許可權,在 store.js 定義一個模組 permission.js,專門用於處理使用者許可權相關的邏輯,使用者許可權列表、選單列表都儲存在此模組;
(5)使用者許可權列表、選單列表的處理,前端的路由要和後端返回的許可權有一個唯一標識(一般用路由名做識別符號),根據此標識篩選出對應的路由。
(6)左側選單,要和使用者資訊、使用者管理模組使用的選單資訊一致,統一使用儲存在 store 中的變數。

三、具體實現流程

1、準備工作,路由定義

/* router/indes.js */
/* 初始路由 */
let router = new Router({
  mode: 'history',routes: [
    {
      path: '/login',name: 'login',component: () => import('@/views/login.vue'),},]
});
/* router/indes.js */
/* 準備動態新增的路由 */
export const dynamicRoutes = [
  {
    path: '/',name: 'home',component: () => import('@/views/home.vue'),meta: {
      requiresAuth: true,children: [
      // 使用者資訊
      {
        path: '/user-info',name: 'user-info',component: () => import('@/views/user-setting/user-info.vue'),// 修改密碼
      {
        path: '/user-password',name: 'user-password',component: () => import('@/views/user-setting/user-password.vue'),]
  },{
    path: '/403',component: () => import('@/views/error-page/403'),{
    path: '*',component: () => import('@/views/error-page/404'),];

系統主要頁面的路由,後續會將這些路由經過許可權篩選,新增到 home 路由的 children 裡面

/* router/router.js */
export default [
  // 部署管理
  {
    path: '/deploy-manage',name: 'deploy-manage',component: () => import('@/views/sys-admin/deploy-manage/deploy-manage.vue'),meta: {
      permitName: 'deploy-manage',}
  },// ...
];

2、使用者登入

使用者進入登入頁,輸入使用者名稱、密碼、驗證碼,點選登入,傳送登入請求,登入成功之後,將 token 儲存在 sessionStorage,然後跳轉到首頁 /home ,進入路由攔截的邏輯。

/* login.vue */
// 傳送登入請求
vm.$http.login(params,data => {
  sessionStorage.token = data.token;
  // ...

  // 跳轉到首頁 home。這裡會觸發全域性路由攔截 router.beforeEach
  vm.$router.push({ name: 'home' });
},err => {
  console.log(err);
});

3、全域性路由攔截

首先從開啟本地服務 http://localhost:2001 開始,開啟後會進入 login 頁面,那麼判斷的依據是什麼?

首先是 token。沒有登入的使用者是拿不到 token 的,而登入後的使用者我們會將 token 存到 seesionStorage,因此,根據當前有沒有 token 即可知道是否登入。

/* 全域性路由攔截 */
router.beforeEach((to,from,next) => {
  // 根據有沒有token判斷是否登入
  if (!sessionStorage.token) {
    // 1、當用戶開啟localhost,to.matched === [],匹配的是空路由,此時需要重定向到login
    // 2、重定向到login之後,to.matched === [name: "login",path: "/login"...] 就是上一步的login頁面
    // to.matched.some(item => item.meta.requiresAuth) 這句的意思是 進入的路由頁需要登入認證,取反就是不用登入,直接通過
    if (to.matched.length > 0 && !to.matched.some(item => item.meta.requiresAuth)) {
      next(); // 跳過,進入下一個導航鉤子。比如:在 /login 路由頁重新整理頁面會走到此邏輯
    } else {
      next({ path: '/login' });
    }
  } else {
    // 現在有token了
    if (!store.state.permission.permissionList) {
      // 如果沒有 permissionList,發請求獲取使用者許可權列表
      store.dispatch('permission/FETCH_PERMISSION').then(() => {
        next({ path: to.path,query: to.query });
      });
    } else {
      // 現在有 permissionList 了
      if (to.path !== '/login') {
        if (to.matched.length === 0) {
          // 如果匹配到的路由形如 https://172.24.1.117/?id=xxx&name=xxx,表明是關聯跳轉時沒有許可權,跳轉到403
          next({ path: '/403' });
        } else if (queryChange) {
          // 跳轉之前將路由中查詢字串為空的過濾掉,如 xxx.com?page=&size= 這種
          next({ name: to.name,params: to.params,query: to.query });
        } else if (sessionStorage.isSysLock === 'true' && to.path !== '/sys-lock') {
          next({ path: '/sys-lock' });
        } else {
          next();
        }
      } else {
        // 1.如果使用者手動在位址列輸入 /login,重定向到之前的路由頁
        // next(from.fullPath);

        // 2.如果使用者手動在位址列輸入 /login,清除token並重新整理頁面,就會去到登入頁
        store.commit('goToLogin');
      }
    }
  }
});

(1)當用戶開啟 localhost,此時還沒有 token,匹配的是空路由,我們重定向到登入頁 next({ path: '/login' });
(2)使用者在登入頁重新整理頁面,也會進入路由攔截,此時匹配的是 login 路由,而 login 路由是不需要登入驗證的(requiresAuth 為空或者 false),所以直接跳過執行 next();
(3)使用者在登入頁輸入了使用者名稱和密碼,登入成功,儲存了 token,跳轉到 /home 路由;
(4)此時進入路由攔截,已經有 token了,但是還沒有使用者許可權 permissionList,然後發請求獲取使用者許可權列表,得到許可權後 next({ path: to.path,query: to.query }); 繼續往下走;
(5)再次進入路由攔截,此時有 token 和 permissionList 了,就可以根據實際業務進行跳轉了。上面的程式碼是判斷當前是不是 login 路由,如果使用者登入後手動在位址列輸入 /login,則清除 token 跳轉到登入頁。其他的邏輯就跟具體業務相關了,就不細講了。

4、處理使用者許可權

處理使用者許可權,在 store.js 定義一個模組 permission.js,專門用於處理使用者許可權相關的邏輯,使用者許可權列表、選單列表都儲存在此模組;
來看看 permission.js 主要做了什麼:

/* permission.js */
/* 由於許可權這塊邏輯很多,所以在vuex添加了一個permission模組來處理許可權相關的邏輯和變數 */
import httpRequest from '@/assets/js/service/http'; // http請求
import handleModule from '@/assets/js/common/handle-module'; // 處理路由、側邊欄的公共函式
import router,{ dynamicRoutes } from '@/router/index'; // 預設路由配置,動態路由配置
import permissionRouter from '@/router/router'; // 需要許可權的路由配置
// ...
export default {
  // ...
  actions: {
    async FETCH_PERMISSION({ commit,state }) {
      // 初始化路由表,注意這裡必須寫,router.beforeEach 路由攔截時,多次執行 FETCH_PERMISSION
      commit('setPermission',[]);

      // 發請求獲取後端返回的使用者許可權
      let data = await getUserByToken();
      let userPopedoms = data.userPopedoms || [];

      // 儲存使用者的許可權模組(去除掉使用者管理和登入),使用者管理模組可以使用,許可權列表
      let userPopeList = userPopedoms.filter(v => v.requestMapping !== 'user-manage' && v.requestMapping !== 'login');
      commit('setUserPopedoms',userPopeList);

      // 根據許可權篩選出我們設定好的路由並加入到 path='/' 的children,就是home路由的children下
      let routes = handleModule.getRouter(userPopedoms,permissionRouter);
      let homeContainer = dynamicRoutes.find(v => v.path === '/');

      // 使用concat的目的是讓 分配給使用者的許可權處於 children 的第0項
      homeContainer.children = routes.concat(homeContainer.children);
      // 設定首頁重定向,重定向到使用者許可權的第0項
      homeContainer.redirect = homeContainer.children[0].name;

      // 根據許可權生成左側導航選單
      let sidebarMenu = handleModule.getSidebarMenu(userPopeList);
      commit('setMenu',sidebarMenu);

      // 初始路由
      let initialRoutes = router.options.routes;
      // 動態新增路由。只有重新整理頁面才會清空動態新增的路由資訊
      router.addRoutes(dynamicRoutes);
      // 完整的路由表
      commit('setPermission',[...initialRoutes,...dynamicRoutes]);
    }
  },};

(1)首先,let data = await getUserByToken(); 發請求獲取使用者許可權,得到 data,data.userPopedoms 格式大致如下:

[
 {
  "moduleGroupId": 1001,"moduleGroupName": "部署管理","requestMapping": "deploy-manage",{
  "moduleGroupId": 1100,"moduleGroupName": "系統管理","requestMapping": "sys-manage","moduleList": [
   {
    "moduleId": 1101,"moduleName": "系統日誌","requestMapping": "system-log","moduleGroupId": 1100,{
    "moduleId": 1102,"moduleName": "系統告警","requestMapping": "sys-alert",],}
]

(2)然後,根據我們寫好的路由陣列,進行對比,過濾得到我們要的路由。路由格式在上文“路由定義”的 router/router.js 已經提到。還要根據使用者許可權處理得到側邊欄選單。

為此,我們需要兩個處理函式,一個根據使用者許可權列表和路由陣列過濾得到最終路由,另一個根據使用者許可權處理得到側邊欄選單。所以另外專門建立了一個檔案 handle-module.js 存放這兩個函式。

/* handle-module.js */
const handleModule = {
  /**
   * 根據後臺返回的許可權,以及配置好的所有路由,過濾出真實路由
   * @param {Array} permissionList 後臺返回的使用者許可權列表
   * @param {Array} allRouter 前端配置好的所有動態路由的集合
   * @return {Array} 過濾後的路由
   */
  getRouter(permissionList = [],allRouter = []) {
    // permissions 的格式為 ["deploy-manage","system-log"]
    let permissions = permissionList.reduce((acc,cur) => {
      if (cur.moduleList && cur.moduleList.length > 0) cur = cur.moduleList;
      return acc.concat(cur);
    },[]).map(v => v.requestMapping);

    return allRouter.filter(item => permissions.includes(item.meta.permitName));
  },/**
   * 根據後臺返回的許可權,生成側邊欄
   * @param {Array} permissionList 後臺返回的使用者許可權列表
   * @return {Array} sidebarMenu 生成的側邊欄陣列
   */
  getSidebarMenu(permissionList = []) {
    let sidebarMenu = [];
    permissionList.forEach(item => {
      let menuItem = {
        name: item.requestMapping,title: item.moduleGroupName,};
      menuItem.children = (item.moduleList || []).map(child => ({
        name: child.requestMapping,title: child.moduleName,}));
      sidebarMenu.push(menuItem);
    });

    return sidebarMenu;
  }
};
export default handleModule;

(3)上面得到過濾後的路由陣列後,加入到 path 為 '/' 的 children 下面

{
    path: '/',children: [
      /* 將上面得到的路由加入到這裡 */
      // 使用者資訊
      {
        path: '/user-info',]
}

(4)上面根據許可權生成側邊欄選單之後,儲存在 store 待用。

(5)上面第三步將動態路由加入到 home 的 children 之後,就可以將 dynamicRoutes 加入到路由中了。router.addRoutes(dynamicRoutes);

(6)到了這裡,路由就新增完了,也就是 FETCH_PERMISSION 操作完畢了,就可以在 action.then 裡面呼叫 next({ path: to.path,query: to.query }); 進去路由,也就是進入 home。我們上面已經將 home 路由重定向為選單的第一個路由資訊,所以會進入系統選單的第一個頁面。

重新整理頁面後,根據 router.beforeEach 的判斷,有 token 但是沒有 permissionList ,會重新觸發 action 去發請求獲取使用者許可權,之前的邏輯會重新走一遍,所以沒有問題。

退出登入後,需要清除 token 並重新整理頁面。因為是通過 addRoutes 新增路由的,而 vue-router 沒有刪除路由的 api,所以清除路由、清除 store 中儲存的各種資訊,重新整理頁面是最保險的。

相關檔案的目錄截圖:

淺談vue許可權管理實現及流程

四、總結

缺點:
全域性路由守衛裡,每次路由跳轉都要做判斷;
每次重新整理頁面,需要重新發請求獲取使用者許可權;
退出登入時,需要重新整理一次頁面將動態新增的路由以及許可權資訊清空;

優點:
選單與路由分離,選單的修改、新增、刪除由後端控制,利於後期維護;
使用 addRoutes 動態掛載路由,可控制使用者不能在 url 輸入相關地址進行跳轉;

vue許可權管理還有其他實現方式,大家可以根據實際業務考慮做調整,以上的實現方式是比較適合我們現有專案的需求的。以上,有問題歡迎提出交流,喜歡的話點個贊哦~

到此這篇關於淺談vue許可權管理實現及流程的文章就介紹到這了,更多相關vue許可權管理內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!