<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端權(quán)限開發(fā)——設(shè)計到實踐(保姆級)

          共 20977字,需瀏覽 42分鐘

           ·

          2024-05-10 08:38

          關(guān)于本文

          作者:守夜人x

          https://juejin.cn/post/7259210874446692411

          1.權(quán)限控制的方案選擇。

          做后臺項目區(qū)別于做其它的項目,權(quán)限驗證與安全性是非常重要的,可以說是一個后臺項目一開始就必須考慮和搭建的基礎(chǔ)核心功能。在后臺管理系統(tǒng)中,實現(xiàn)權(quán)限控制可以采用多種方案:

          權(quán)限方案類型 描述
          基于角色的訪問控制(Role-Based Access Control,RBAC) 是一種廣泛采用的權(quán)限控制方案。系統(tǒng)中定義了不同的角色,每個角色具有一組權(quán)限,而用戶被分配到一個或多個角色。通過控制用戶角色的分配,可以實現(xiàn)對用戶訪問系統(tǒng)中不同功能和資源的權(quán)限控制。
          基于權(quán)限的訪問控制(Permission-Based Access Control) 這種方案將權(quán)限直接分配給用戶,而不是通過角色來管理。每個用戶都有自己的權(quán)限列表,控制用戶對系統(tǒng)中各項功能和資源的訪問。
          基于資源的訪問控制(Resource-Based Access Control,RBAC) 這種方案將權(quán)限控制與資源本身關(guān)聯(lián)起來。系統(tǒng)中的每個資源都有自己的訪問權(quán)限,用戶通過被授予資源的訪問權(quán)限來控制其對資源的操作。
          層次結(jié)構(gòu)權(quán)限控制(Hierarchical Access Control) 這種方案基于資源和操作的層次結(jié)構(gòu)來進(jìn)行權(quán)限控制。系統(tǒng)中的資源和操作被組織成層次結(jié)構(gòu),用戶被授予訪問某個層次及其子層次的權(quán)限。
          基于規(guī)則的訪問控制(Rule-Based Access Control) 這種方案使用預(yù)定義的規(guī)則來確定用戶對系統(tǒng)中功能和資源的訪問權(quán)限。規(guī)則可以基于用戶屬性、環(huán)境條件或其他因素進(jìn)行定義。

          這里我選擇了基于角色的訪問控制(Role-Based Access Control,RBAC) 這是因為RBAC提供了一種靈活且易于管理的方式來控制用戶對系統(tǒng)功能和資源的訪問,也是目前最主流的前端權(quán)限方案選擇。

          在RBAC中,系統(tǒng)中的功能和資源被組織成角色,而用戶則被分配到不同的角色。每個角色都有一組權(quán)限,定義了該角色可以執(zhí)行的操作和訪問的資源。通過給用戶分配適當(dāng)?shù)慕巧梢詫崿F(xiàn)對用戶的權(quán)限控制。

          RBAC的好處之一是它簡化了權(quán)限管理的復(fù)雜性。管理員只需管理角色和分配角色給用戶,而不需要為每個用戶單獨(dú)定義權(quán)限。當(dāng)需要對用戶的權(quán)限進(jìn)行修改時,只需調(diào)整其角色的權(quán)限即可。

          此外,RBAC還支持靈活的權(quán)限組合,允許創(chuàng)建具有不同權(quán)限組合的角色,以適應(yīng)不同用戶的需求。它也便于擴(kuò)展,可以隨著系統(tǒng)的發(fā)展和需求的變化而調(diào)整和添加角色。

          2.RBAC下的權(quán)限字段設(shè)計與管理模型

          1.用戶權(quán)限授權(quán)

          是對用戶身份認(rèn)證的細(xì)化。可簡單理解為訪問控制,在用戶身份認(rèn)證通過后,系統(tǒng)對用戶訪問菜單或按鈕進(jìn)行控制。也就是說,該用戶有身份進(jìn)入系統(tǒng)了,但他不一定能訪問系統(tǒng)里的所有菜單或按鈕,而他只能訪問管理員給他分配的權(quán)限菜單或按鈕。
          主要包括:

          • Permission(權(quán)限標(biāo)識、權(quán)限字符串):針對系統(tǒng)訪問資源的權(quán)限標(biāo)識,如:用戶添加、用戶修改、用戶刪除。

          • Role (角色):可以理解為權(quán)限組,也就是說角色下可以訪問和點(diǎn)擊哪些菜單、訪問哪些權(quán)限標(biāo)識。

          權(quán)限標(biāo)識或權(quán)限字符串校驗規(guī)則:

          • 權(quán)限字符串:指定權(quán)限串必須和菜單中的權(quán)限標(biāo)識匹配才可訪問

          • 權(quán)限字符串命名規(guī)范為:模塊:功能:操作,例如:sys:user:edit

          • 使用冒號分隔,對授權(quán)資源進(jìn)行分類,如 sys:user:edit 代表 系統(tǒng)模塊:用戶功能:編輯操作

          • 設(shè)定的功能指定的權(quán)限字符串與當(dāng)前用戶的權(quán)限字符串進(jìn)行匹配,若匹配成功說明當(dāng)前用戶有該功能權(quán)限

          • 還可以使用簡單的通配符,如 sys:user:*,建議省略為 sys:user(分離前端不能使用星號寫法)

          • 舉例1 sys:user 將于 sys:user 或 sys:user: 開頭的所有權(quán)限字符串匹配成功

          • 舉例2 sys 將于 sys 或 sys: 開頭的所有權(quán)限字符串匹配成功 這種命名格式的好處有:

          1. 可讀性和可理解性:使用模塊、功能和操作的格式可以直觀地表達(dá)權(quán)限的含義。每個部分都有明確的作用,模塊表示特定的模塊或子系統(tǒng),功能表示模塊內(nèi)的某個功能或頁面,操作表示對功能進(jìn)行的具體操作。通過這種格式,權(quán)限名稱可以更容易地被開發(fā)人員、管理員和其他人員理解和解釋。

          2. 可擴(kuò)展性和靈活性: 通過使用模塊、功能和操作的格式,可以輕松地擴(kuò)展和管理權(quán)限。每個模塊、功能和操作都可以被單獨(dú)定義和控制。當(dāng)系統(tǒng)需要增加新的功能或操作時,可以根據(jù)需要添加新的權(quán)限字符串,而不需要修改現(xiàn)有的權(quán)限規(guī)則和代碼。

          3. 細(xì)粒度的權(quán)限控制: 這種格式支持細(xì)粒度的權(quán)限控制,可以針對特定的功能和操作進(jìn)行權(quán)限管理。通過將權(quán)限名稱拆分為模塊、功能和操作,可以精確地定義哪些用戶或角色具有訪問或操作特定功能的權(quán)限。

          4. 避免權(quán)限沖突: 使用模塊、功能和操作的格式可以避免權(quán)限之間的沖突。不同模塊、功能和操作的權(quán)限名稱是唯一的,這樣可以避免同名權(quán)限之間的混淆和沖突。

          2.權(quán)限管理模型

          關(guān)鍵數(shù)據(jù)模型如下:

          • 用戶:登錄賬號、密碼、角色

          • 角色:角色名稱、角色權(quán)限字符、對應(yīng)菜單、對應(yīng)菜單下的權(quán)限

          • 菜單:菜單名稱、菜單URL、菜單類型

          • 用戶角色關(guān)系:用戶編碼、角色編碼

          • 角色菜單關(guān)系:角色編碼、菜單編碼

          關(guān)系圖如下:

             

          rust

          復(fù)制代碼

          【用戶】 <---多對多---> 【角色】 <---多對多---> 【菜單/權(quán)限】

          3.實現(xiàn)思路與步驟

          前端權(quán)限一般分為路由級權(quán)限和按鈕級權(quán)限,這里我們先實現(xiàn)頁面路由級的權(quán)限功能,按鈕級的會在后面講到。大致的思路如下圖:

           上圖是用戶從登錄-->路由守衛(wèi)-->權(quán)限驗證-->構(gòu)建路由表-->跳轉(zhuǎn)目標(biāo)頁面的一個簡單的正向流程,可以看到,權(quán)限驗證構(gòu)建路由表這兩步是發(fā)生在路由守衛(wèi)這一步里,而實際上在開發(fā)設(shè)計階段,我們還要做的準(zhǔn)備有:

          1. 與后端確認(rèn)權(quán)限字段及類型

          2. 確定路由表信息是由前端還是后端生成

          為了方便理解,我們就按照這個正向流程來逐步實現(xiàn),期間需要用到或者提前考慮設(shè)計到的內(nèi)容,包括以上需要準(zhǔn)備的兩點(diǎn),我會補(bǔ)充在步驟當(dāng)中。

          1.登錄

          登錄成功后,獲取到token,將token存到本地的sessionStorage里。

             

          action.js

          復(fù)制代碼

          async login({ commit }, userInfo) {
          const { data } = await login(userInfo)
          const accessToken = 'Bearer ' + data.access_token
          if (accessToken) {
          sessionStorage.getItem(tokenTableName)
          } else {
          message.error(`登錄接口異常,未正確返回${tokenName}...`)
          }
          }

          接著,進(jìn)行路由跳轉(zhuǎn)到首頁

             

          login.vue

          復(fù)制代碼

          import { useRouter } from 'vue-router';
          const router = useRouter();
          router.push('/');

          如果考慮到頁面token失效后,重新登陸后返回原先的路由地址,則需要在路由守衛(wèi)中添加redirect字段用來存儲當(dāng)前的路由地址

             

          permissions.js

          復(fù)制代碼

          next({ path: '/login', query: { redirect: to.fullPath }, replace: true })

          然后在登錄頁中,監(jiān)聽路由對象中的這個值,如果有值,那么在剛剛路由跳轉(zhuǎn)時,就跳轉(zhuǎn)到該路由地址,而非/首頁,另外,還需要考慮到403,404頁面。所以,添加了這些邏輯后,部分代碼如下:

             

          js

          復(fù)制代碼

          //login.vue
          <script setup>
          import { ref, watch, useRouter } from 'vue';
          import { useRouter } from 'vue-router';
          const redirect = ref('/');
          const router = useRouter();

          watch(
          () => router.currentRoute.value.query.redirect,
          (redirectValue) => {
          redirect.value = redirectValue || '/';
          },
          { immediate: true }
          );

          function handleRoute() {
          return redirect.value === '/404' || redirect.value === '/403'
          ? '/'
          : redirect.value
          },

          async handleSubmit() {
          loading.value = true
          try {
          await login(form.value)
          loading.value = false
          router.push(handleRoute());
          } catch {
          loading.value = false
          }
          },
          </script>


          2.路由守衛(wèi)與權(quán)限校驗

          當(dāng)進(jìn)行路由跳轉(zhuǎn)時,會進(jìn)入到路由守衛(wèi)中,在路由守衛(wèi)中:

          1. 路由守衛(wèi)會首先判斷有沒有token,如果沒有,先判斷要去的路由地址是否包含在路由白名單內(nèi),如果要去的路由地址也不在路由白名單內(nèi),就會讓用戶跳轉(zhuǎn)到登錄頁重新登陸。

          2. 如果有token,會先判斷跳轉(zhuǎn)的目標(biāo)地址是否是登錄頁,如果是,則重新跳轉(zhuǎn)到默認(rèn)首頁

          3. 此時,我們就需要對用戶權(quán)限進(jìn)行校驗,首先,判斷當(dāng)前用戶是否擁有角色信息,如果沒有,就要獲取用戶的角色信息。

          3.獲取角色信息與權(quán)限信息

          調(diào)取用戶信息接口獲取用戶角色信息和權(quán)限信息,代碼如下:

             

          js

          復(fù)制代碼

          //store/user
          // 獲取用戶信息
          GetInfo({ commit, dispatch }) {
          return new Promise((resolve, reject) => {
          getUserInfo()
          .then(async (response) => {
          const result = response.data
          if (result.roles && result.roles.length > 0) {
          // 驗證返回的roles是否是一個非空數(shù)組
          commit('SET_ROLES', result.roles)
          //permissions就是對應(yīng)的用戶權(quán)限信息
          commit('SET_PERMISSION', result.permissions)
          } else {
          // 如果當(dāng)前用戶沒有角色,則賦值一個默認(rèn)角色
          commit('SET_ROLES', ['ROLE_DEFAULT'])
          }
          resolve(result)
          // GetInfo一旦失敗就說明這個token不是過期就是丟失了,直接走catch并讓調(diào)用方跳轉(zhuǎn)路由
          if (!response.success) {
          reject(response.errorMsg)
          }
          })
          .catch((error) => {
          reject(error)
          })
          })
          },

          根據(jù)之前上文提到的權(quán)限管理模型,從之間的關(guān)系是多對多,因此,role(角色)permssions(權(quán)限)的類型應(yīng)為數(shù)組,其中的權(quán)限permssion這個字段的格式規(guī)范也在上文提及。在與后端約定好后,后端返回的信息如圖:

          4.誰來生成動態(tài)路由表?

          在成功獲取到用戶信息,拿到用戶角色和對應(yīng)權(quán)限后,此時,就需要根據(jù)權(quán)限生成對應(yīng)的路由表了,到這里,我們需要思考一個問題:
          路由表是由后端提供還是由前端提供?
          A:前端根據(jù)權(quán)限生成路由表
          B:后端生成路由表給前端

          沒錯!答案是C:路由表可以由后端提供或由前端提供。 

          兩者的優(yōu)劣分別是:

          1. 后端提供路由表:

            • 前后端耦合度高:由于路由表由后端提供,前端開發(fā)人員可能需要與后端開發(fā)人員密切協(xié)作,增加了協(xié)調(diào)和溝通的成本。

            • 前端依賴后端:前端應(yīng)用程序可能需要等待后端提供路由表后才能進(jìn)行開發(fā)和測試,增加了開發(fā)的時間和依賴性。

            • 安全性高:后端負(fù)責(zé)驗證和控制權(quán)限,可以確保只有授權(quán)的用戶能夠訪問特定的路由和功能。

            • 隱藏敏感信息:后端可以根據(jù)用戶的角色和權(quán)限隱藏不應(yīng)該被訪問的敏感路由和數(shù)據(jù)。

            • 適用于復(fù)雜的權(quán)限規(guī)則:后端可以使用更復(fù)雜的邏輯和規(guī)則來處理權(quán)限控制,例如基于用戶角色、用戶組、權(quán)限等的復(fù)雜控制邏輯。

            • 優(yōu)點(diǎn):

            • 缺點(diǎn):

          2. 前端提供路由表:

            • 安全性較低:前端提供的路由表容易受到篡改和繞過,安全性相對較低。

            • 隱藏敏感信息的難度增加:前端無法直接隱藏敏感路由和數(shù)據(jù),需要依賴后端接口的授權(quán)驗證來保護(hù)敏感信息。

            • 前后端解耦:前端可以獨(dú)立開發(fā)和維護(hù)路由表,減少了與后端的依賴性和協(xié)調(diào)成本。

            • 更好的用戶體驗:前端可以根據(jù)用戶的角色和權(quán)限動態(tài)展示路由和功能,提供更靈活和個性化的用戶體驗。

            • 優(yōu)點(diǎn):

            • 缺點(diǎn):

          最終,考慮到后臺管理系統(tǒng)的安全性優(yōu)先級最高,我選擇了由后端存儲,并生成返回路由表信息。 確定好了之后,你就會遇到一個坑:后端提供的路由表無法直接在前端路由中添加使用 這是因為,后端存儲的路由表的結(jié)構(gòu)只一個JSON對象。怎么解決,我們放到后面講。

          5.公共路由和動態(tài)路由

          我們現(xiàn)在把整個路由分為兩個部分,分別是公共路由動態(tài)路由(私有路由)

          公共路由

          公共路由顧名思義就是無論當(dāng)前用戶是什么角色,都會出現(xiàn)的路由部分。一般這樣的路由分別有:首頁駕駛艙、登錄注冊頁、403頁、404頁。 這部分路由是在前端項目中提前寫死的。
          以我的項目為例,我在src/config下創(chuàng)建一個publicRouter.js文件,然后里面放入基礎(chǔ)路由信息:

             

          javascript

          復(fù)制代碼

          //基礎(chǔ)公共路由
          export const constantRouterMap = [
          {
          path: '/',
          name: '',
          component: () => import('@/layout'),
          redirect: '/homePage',
          children: [
          {
          path: '/homePage',
          name: 'homePage',
          component: () => import('@/views/homePage/index.vue'),
          },
          ],
          },
          {
          path: '/login',
          component: () => import('@/views/login'),
          },
          {
          path: '/403',
          name: '403',
          component: () => import('@/views/403'),
          },
          {
          path: '/:pathMatch(.*)*',
          component: () => import('@/views/404'),
          }]

          這里面都是路由懶加載的寫法,這個沒啥好說的。需要注意的是在vue3中,vue-router的版本是4.x以上,在跳轉(zhuǎn)404頁面時,如果你的寫法是

             

          css

          復(fù)制代碼

          {
          path: "/404",
          name: "notFound",
          component: () => import('@/views/404')
          }

          這樣寫會提示報錯,這是因為vue-router4.x的版本官方推薦引入了這種新寫法,配置一個通配符路由,匹配所有未被其他具體路由匹配的路徑。其中 :pathMatch 是參數(shù)名,而 (.*)* 是參數(shù)的匹配模式,它使用正則表達(dá)式來匹配任意路徑。

          動態(tài)路由(私有路由)

          上文提到的后端返回給前端當(dāng)前用戶的路由表信息,其實就是私有路由,這部分的路由是動態(tài)的。那么我們只需要把公共路由+動態(tài)路由=當(dāng)前角色用戶完整路由表。然鵝,后端返回給前端的動態(tài)路由是無法直接添加到當(dāng)前頁面路由中的,這是因為在瀏覽器的前后端請求當(dāng)中,信息的返回體都是以JSON字符串的數(shù)據(jù)交換格式進(jìn)行傳輸,而JSON字符串是不支持函數(shù)的。為什么要提到函數(shù)形式呢?
          因為當(dāng)我們在路由表使用懶加載的寫法時,component的值是一個異步函數(shù)。也只有異步函數(shù)才能實現(xiàn)對路由的異步懶加載。import()會返回一個promise,這個 promise 最終會加載對應(yīng)的組件模塊。在格式上表現(xiàn)為一個func函數(shù),因此,我們無法將compnent的值傳給后端存儲
          但是辦法總比困難多,我們可以將對應(yīng)的路徑字符串傳給后端存儲,然后再通過后端返回的路徑字符串轉(zhuǎn)換成這種箭頭函數(shù)的寫法。
          除了這個需要轉(zhuǎn)換以外,我們還要考慮一些問題:

          1. 比如,轉(zhuǎn)換后的路由結(jié)構(gòu)要符合router中的路由格式,否則會在調(diào)用router.addRoutes時報錯,添加失敗。

          2. 添加一些其他自定義屬性(例如添加導(dǎo)航欄菜單圖標(biāo)、某個路由的顯隱、路由緩存、跳轉(zhuǎn)外鏈接、...),以滿足特定需求。
            以我項目中的的路由示例,直接上代碼看:

             

          json

          復(fù)制代碼

          {
          "path": "/", //path,路由的路徑,即訪問該路由時的 URL 路徑。
          "name": "homePage",//name,路由的名稱,用于在代碼中標(biāo)識路由。通常用于編程式導(dǎo)航。
          "hidden": false,//hidden,是否隱藏路由,在Vue Router 4.0 中被廢棄。
          "component": "Layout"//路由對應(yīng)的組件,可以是通過懶加載方式導(dǎo)入的異步組件,或直接引入的同步組件。
          "meta": {//meta,路由元信息,可以用于存儲一些額外的信息,比如頁面標(biāo)題、權(quán)限等
          "title": "首頁", //路由標(biāo)題
          "icon": "icon-Home", //路由圖標(biāo)
          "target": "", //是否跳轉(zhuǎn)新頁簽
          "permission": "homePage",//權(quán)限
          "keepAlive": false//是否緩存
          },
          "redirect": "/homePage",//重定向
          "fullPath": "/",//完整的url路徑,會帶上?后面的參數(shù)
          "children": [//子路由
          {
          "path": "/homePage",
          "name": "homePage",
          "meta": {
          "title": "首頁",
          "icon": "",
          "target": "",
          "permission": "homePage",
          "keepAlive": false
          },
          "fullPath": "/homePage"
          }
          ]
          }

          考慮完這些之后,我們就開始拿著后端返回的路由表信息動手轉(zhuǎn)換吧! 先看看后端返回給前端的路由表格式內(nèi)容

           這里你只需要注意component這個字段的值就行了。
          可能有人會問:“那我給后端傳的路由表是什么格式的,也是這樣嗎?”
          答案是:當(dāng)然不是!RBAC權(quán)限方案中,用戶可以自己配置權(quán)限,也就是會有對應(yīng)的用戶管理,角色管理,菜單(權(quán)限)管理這三個基本的模塊,給后端傳的是對應(yīng)模塊的修改配置內(nèi)容。前端是不需要關(guān)心傳給后端什么格式。 有點(diǎn)啰嗦了,總之,就是你指著上文這三張圖片,讓后端給你生成出來就完事了。后端要是說辦不到,那就是后端的問題。


          6.獲取動態(tài)路由(私有路由)并轉(zhuǎn)換

          接下來,這一步是整個前端權(quán)限中最重要的一步。在src/router下,我們新建一個generatorRouters.js文件。里面的內(nèi)容是:

             

          js

          復(fù)制代碼

          //getCurrentUserNav獲取動態(tài)路由的接口
          import { getCurrentUserNav } from '@/api/user'
          //validURL是一個正則判斷方法,用來校驗當(dāng)前字符串是否符合url外鏈格式
          import { validURL } from '@/utils/validate'
          // 前端路由表
          const constantRouterComponents = {
          // 基礎(chǔ)頁面 layout 必須引入
          Layout: () => import('@/layout'),
          403: () => import('@/views/403'),
          404: () => import('@/views/404'),
          }


          /**
          * 動態(tài)生成菜單
          * @param token
          * @returns {Promise<Router>}
          */
          export const generatorDynamicRouter = (token) => {
          return new Promise((resolve, reject) => {
          getCurrentUserNav()
          .then((res) => {
          //接收后端返回的路由表,結(jié)構(gòu)是個數(shù)組
          let menuNav = res.data
          //將路由表存到本地臨時緩存中
          //這一步是為了刷新的時候不需要再調(diào)接口,增加用戶體驗的,沒這方面需求可以不用寫
          sessionStorage.setItem('generateRoutes', JSON.stringify(menuNav))
          //轉(zhuǎn)化路由格式
          const routers = generator(menuNav)
          resolve(routers)
          })
          .catch((err) => {
          reject(err)
          })
          })
          }

          /**
          * 格式化樹形結(jié)構(gòu)數(shù)據(jù) 生成 vue-router 層級路由表
          *
          * @param routerMap //當(dāng)前路由表route
          * @param parent//當(dāng)前路由表route的父級component
          * @returns {*}
          */
          export const generator = (routerMap, parent) => {
          return routerMap.map((item) => {
          const { title, show, hideChildren, hiddenHeaderContent, icon, hidden } =
          item.meta || {}
          if (item.component) {
          // Layout ParentView 組件特殊處理
          //這里是對父組件/布局組件的處理,因為有可能出現(xiàn)嵌套布局和多種布局的情況,這個時候需要進(jìn)行處理。
          if (item.component === 'Layout') {
          item.component = 'Layout'
          } else if (item.component === 'ParentView') {
          item.component = 'BasicLayout'
          item.path = '/' + item.path
          }
          }
          if (item.isFrame === 0) {
          item.target = '_blank'
          }
          const currentRouter = {
          // 如果路由設(shè)置了 path,則作為默認(rèn) path,否則 路由地址 動態(tài)拼接生成如 /dashboard/workplace
          path: item.path || `${(parent && parent.path) || ''}/${item.key}`,
          // 路由名稱,建議唯一
          // name: item.name || item.key || '',
          name: item.name || item.key || '',
          // 該路由對應(yīng)頁面的 組件 :方案1
          // 該路由對應(yīng)頁面的 組件 :方案2 (動態(tài)加載)
          //這里就是將component的字符串值轉(zhuǎn)換成懶加載的異步函數(shù)
          //同時,如果當(dāng)前路徑并沒有對應(yīng)的組件,catch捕獲報錯,然后跳轉(zhuǎn)到404頁,這一步很重要,否則
          component:
          constantRouterComponents[item.component || item.key] ||
          (() =>
          import(`@/views/${item.component}`).catch(() =>
          import('@/views/404')
          )),
          hidden: item.hidden,
          // redirect: '/' + item.path || `${parent && parent.path || ''}/${item.key}`,
          // meta: 頁面標(biāo)題, 菜單圖標(biāo), 頁面權(quán)限(供指令權(quán)限用,可去掉)
          meta: {
          title: title,
          icon: icon,
          hiddenHeaderContent: hiddenHeaderContent,
          target: validURL(item.path) ? '_blank' : '',
          permission: item.name,
          keepAlive: false,
          hidden: hidden,
          },
          //適配本框架的跳轉(zhuǎn)路徑
          }
          // 是否設(shè)置了隱藏菜單
          if (show === false) {
          currentRouter.hidden = true
          }
          // 修正路徑,而antdv-pro的pro-layout要求每個路徑需為全路徑
          //這一步的修正路徑也是因人而異,正常情況下按照我這樣拼接就沒問題了
          if (!constantRouterComponents[item.component || item.key]) {
          if (parent && parent.path && parent.path !== '/') {
          currentRouter.path = `${parent.path}/${item.path}`
          }
          }
          // 是否設(shè)置了隱藏子菜單
          if (hideChildren) {
          currentRouter.hideChildrenInMenu = true
          }
          // 重定向
          item.redirect && (currentRouter.redirect = item.redirect)
          //添加fullPath
          currentRouter.fullPath = currentRouter.path
          // 是否有子菜單,并遞歸處理
          if (item.children && item.children.length > 0) {
          currentRouter.children = generator(item.children, currentRouter)
          }
          return currentRouter
          })
          }

          generatorDynamicRouter方法(promise嵌套)

          上面的代碼有點(diǎn)多,不要覺得麻煩,其實就兩個方法,第一個方法generatorDynamicRouter會調(diào)取后端接口,獲取后端返回的私有路由表信息,返回的是個promise對象,因為需要調(diào)接口請求后端數(shù)據(jù),并且其他方法依賴這個方法的接口返回值,所以是個異步函數(shù)。之所以這么寫的另外一個原因,是因為在它之前還有個promise異步方法包著它。
          我們都知道:在嵌套的 Promise 中,內(nèi)部的 Promise 會先執(zhí)行,然后再執(zhí)行外部的 Promise。這是由于 JavaScript 的異步執(zhí)行機(jī)制所決定的。 所以這種套娃式的目的只有一個——就是確保外層的異步方法在調(diào)用內(nèi)部異步方法時,能夠保證拿到內(nèi)部異步方法請求成功之后的返回值(也就是將異步方法變成同步方法,這也是Promise主要的作用之一,面試常問的)。

          而最外層的異步方法,是放在路由守衛(wèi)當(dāng)中直接調(diào)用的,由于我們是動態(tài)異步加載路由,那么執(zhí)行的方法肯定也是異步。具體的原因是因為:
          如果動態(tài)加載路由的方法不是異步的,那么在路由守衛(wèi)中使用它將導(dǎo)致它立即執(zhí)行并返回一個 Promise,而不是在路由導(dǎo)航過程中延遲加載。這樣會使得在路由守衛(wèi)中的邏輯無法按預(yù)期執(zhí)行,因為在組件加載完成之前,它依賴的組件可能還沒有被加載,導(dǎo)致出現(xiàn)錯誤或不一致的狀態(tài)。

          從后端獲取路由配置往往涉及到網(wǎng)絡(luò)請求和異步操作,不能保證立即返回所有路由信息。當(dāng)你從后端獲取到路由配置后,需要將其添加到 Vue Router 中,以便在前端應(yīng)用程序中進(jìn)行動態(tài)路由。在這個過程中,你需要使用異步操作來確保在獲取到路由配置后再添加到路由中。

          generator方法(遞歸轉(zhuǎn)化)

          言歸正傳,generatorDynamicRouter方法調(diào)取后端接口,拿到后端返回路由表信息后,會緊接著調(diào)用第二個generator方法,并將路由表信息當(dāng)做參數(shù)傳入。而generator是一個遞歸方法。這個也很好理解,因為路由信息里都會包含children子路由,需要層層遞歸,generator內(nèi)部會對每一個路由進(jìn)行逐層轉(zhuǎn)化,每一步都在代碼中標(biāo)有注釋,我就不再過多贅述了。我們只需要知道:generator的最終目的就是將后端返回的路由表結(jié)構(gòu)轉(zhuǎn)化成符合前端路由格式的路由(聽起來有點(diǎn)繞)。
          附上格式化好之后的路由信息:  再對比下格式化之前的樣子:  可以看到component從字符串變成了函數(shù),path路徑也得到了拼接補(bǔ)充。

          7.存儲格式化好的動態(tài)路由及導(dǎo)航欄菜單信息

          上文提到在generatorDynamicRouter異步方法之外還有一層異步方法,其實我是放在了vuex的store下某個模塊中的actions里(默認(rèn)大家都會用vuex)。具體代碼如下

             

          js

          復(fù)制代碼

          const actions = {
          GenerateRoutes({ commit }) {
          let sidebarList = []
          return new Promise((resolve) => {
          generatorDynamicRouter().then((routers) => {
          //sidebarList是側(cè)邊欄渲染數(shù)據(jù)
          sidebarList = routers
          //routers就是我們要存儲到vuex中的動態(tài)路由
          commit('set_routers', routers)
          //側(cè)邊欄數(shù)據(jù)存到vuex中
          commit('set_sidebarList', sidebarList)
          resolve()
          })
          })
          },
          }

          8.添加動態(tài)路由

          好了,到這一步,我們就拿到了準(zhǔn)備好的公共路由私有路由。這個時候,我們再回到路由守衛(wèi),回顧一下之前的流程:

          1. 路由守衛(wèi)先判斷token

          2. 沒有token去登錄拿token(或者是直接去了白名單里)

          3. 拿到token后判斷有沒有角色信息,沒有角色信息就去請求角色信息(調(diào)接口)

          4. 拿到角色信息后,再去獲取對應(yīng)角色信息的路由表(調(diào)接口)

          5. 將后端返回的路由表信息轉(zhuǎn)換成前端能添加的路由表信息格式

          6. 添加路由

          7. 跳轉(zhuǎn)對應(yīng)路由頁面

          路由守衛(wèi)的代碼如下,直接在src/config目錄下新建文件permissions.js

             

          js

          復(fù)制代碼


          import router from '@/router'
          import store from '@/store'
          import getPageTitle from '@/utils/pageTitle'
          import getInfoRouter from '@/router/getInfoRouter'
          import { loginInterception, recordRoute, routesWhiteList } from '@/config'
          router.beforeEach(async (to, from, next) => {
          let hasToken = store.getters['user/accessToken']
          if (hasToken) {
          if (to.path === '/login') {
          next({ path: '/' })
          } else {
          //權(quán)限校驗
          if (store.getters['user/roles'].length === 0) {
          try {
          //獲取用戶信息,包括角色信息和權(quán)限信息
          await store.dispatch('user/GetInfo')
          //獲取動態(tài)路由表信息,并對路由表進(jìn)行格式轉(zhuǎn)化,然后存到vuex中
          await store.dispatch('async-router/GenerateRoutes')
          //從vuex中拿到動態(tài)路由表數(shù)據(jù)
          let accessRoutes = store.getters['async-router/addRouters']
          //循環(huán)添加路由
          accessRoutes.forEach((route) => {
          //isHttp方法用來判斷是否是外鏈,如果是外鏈,就不添加到當(dāng)前路由表中
          if (!isHttp(route.path)) {
          router.addRoute(route) // 動態(tài)添加可訪問路由表
          }
          })
          //刷新跳轉(zhuǎn)
          next({ ...to, replace: true })
          } catch (e) {
          //登出
          await store.dispatch('user/Logout')
          //記錄登出前的路由地址
          next({ path: '/login', query: { redirect: to.fullPath } })
          }
          } else {
          next()
          }
          }
          } else {
          //判斷是否在白名單中
          if (routesWhiteList.indexOf(to.path) !== -1) {
          next()
          } else {
          //記錄跳轉(zhuǎn)到登錄頁之前的路由地址
          next({ path: '/login', query: { redirect: to.fullPath }, replace: true })
          }
          }
          })
          router.afterEach((to) => {
          //瀏覽器頁簽標(biāo)題
          document.title = getPageTitle(to.meta.title)
          })

          在vue-router4.x版本中,往當(dāng)前的路由router對象中動態(tài)添加路由時,需要使用官方提供的addRoute方法,不能直接對router對象進(jìn)行修改,這是因為vue-router必須是要vue在實例化之前就掛載上去的。addRoute方法傳入的參數(shù)是單個路由對象,所以寫法上我們需要對動態(tài)路由數(shù)組進(jìn)行for循環(huán),在內(nèi)部調(diào)用addRoute方法,從而達(dá)到成功添加多個動態(tài)路由的目的。這里附上官方API地址鏈接:router.vuejs.org/zh/api/inte…

          9.遇到的坑

          這里面有幾個坑需要注意下:

          1. 第一個坑就是在vue3支持的vue-router4.0版本之前,也就是vue2中,動態(tài)添加路由的方式支持的是addRoutes,它的參數(shù)是格式是一個數(shù)組。而到了4.0后,addRoutes這個方法被廢棄,取而代之的是addRoute,它的參數(shù)則是一個路由對象。這兩個方法無論是在傳參類型還是添加相同path時的覆蓋邏輯都不相同。

          2. 第二個坑就是無論是用控制臺還是打斷點(diǎn)的方式,或者是用Vue.js devtools的谷歌插件,都無法在路由守衛(wèi)中獲取到添加完成后的最新router對象,也就是說你會在調(diào)試addRoute加載后的動態(tài)路由表時,發(fā)現(xiàn)與之前為添加路由時的路由表是一樣的,從而無法判斷是否動態(tài)路由添加成功(就很蛋疼)

          3. 第三個坑是 next({ ...to, replace: true })這個方法,當(dāng)添加好動態(tài)路由后,如果不這么寫next,會白屏。addRoute()之后第一次訪問被添加的路由會白屏,這是因為剛剛addRoute()就立刻訪問被添加的路由,然而此時addRoute()沒有執(zhí)行結(jié)束,因而找不到剛剛被添加的路由導(dǎo)致白屏。因此需要從新訪問一次路由才行。
            具體原因見文章鏈接:blog.csdn.net/qq_41912398…

          4. 還有一個關(guān)于vuex的問題,從之前的代碼里不難看出,我們將用戶信息,角色、權(quán)限、動態(tài)路由等都存在了vuex中。由于vuex的特性,用戶會在刷新頁面后,vuex里的數(shù)據(jù)會被清空,這個時候就會導(dǎo)致頁面直接跳轉(zhuǎn)到登錄頁。這肯定是不能接受的,因此,我們需要對vuex里的數(shù)據(jù)在刷新的時候做持久化處理。邏輯也很簡單,監(jiān)聽頁面刷新事件,beforeunload這個方法恰好就能做到,在頁面刷新的時候,將vuex中的數(shù)據(jù)緩存到瀏覽器本地臨時緩存中,然后再在頁面初始化的時候,從本地臨時緩存中取出存入vuex對象中。 附上代碼:(項目根目錄下的的App.vue文件中,這里是vue2的寫法)

             

          js

          復(fù)制代碼

          created() {
          //在頁面加載時讀取sessionStorage里的狀態(tài)信息
          sessionStorage.getItem('userMsg') &&
          this.$store.replaceState(
          Object.assign(
          this.$store.state,
          JSON.parse(sessionStorage.getItem('userMsg'))
          )
          )
          //在頁面刷新時將vuex里的信息保存到sessionStorage里
          window.addEventListener('beforeunload', () => {
          sessionStorage.setItem('userMsg', JSON.stringify(this.$store.state))
          })
          },

          這里再附上src/router目錄下的index.js代碼,方便大家理解路由這塊:

             

          js

          復(fù)制代碼

          import { createWebHistory, createRouter, useRouter } from 'vue-router'
          import { constantRouterMap } from '@/config/router.config.js'
          import { generator } from '@/router/generatorRouters'

          const router = createRouter({
          history: createWebHistory(),
          routes: constantRouterMap,
          scrollBehavior(to, from, savedPosition) {
          if (savedPosition) {
          return savedPosition
          } else {
          return { top: 0 }
          }
          },
          })
          //這里是我為了刷新的時候不調(diào)取獲取路由信息的接口,做的本地緩存,因為刷新一次就要獲取一遍用戶信息和
          //動態(tài)路由表信息太影響用戶體驗了,尤其是在網(wǎng)速差的情況下。沒這方面需求的可以不用管
          let asyncRouterMap = []
          function handleLocalRouter() {
          if (sessionStorage.getItem('generateRoutes')) {
          asyncRouterMap = asyncRouterMap.concat(
          JSON.parse(sessionStorage.getItem('generateRoutes'))
          )
          const generatedRoutes = generator(asyncRouterMap)
          generatedRoutes.push({
          path: '/:pathMatch(.*)*',
          redirect: '/404',
          hidden: true,
          })
          generatedRoutes.forEach((route) => {
          router.addRoute(route)
          })
          }
          }
          handleLocalRouter()
          export default router

          10.結(jié)束與優(yōu)化

          當(dāng)用戶要退出時,也就是登出,我們只需要調(diào)用登出接口,并在接口成功返回后,重置vuex中的用戶信息(token,角色roles,權(quán)限permission),同時清空本地緩存的數(shù)據(jù)就行了。附上代碼:

             

          js

          復(fù)制代碼

          /**

          * @description 退出登錄
          * @param {*} { dispatch }
          */
          Logout({ commit, dispatch, state }) {
          return new Promise((resolve, reject) => {
          logout()
          .then(async () => {
          await dispatch('resetAll')
          //清空導(dǎo)航tab頁簽
          this.dispatch('tagsBar/delAllVisitedRoutes', { root: true })
          resolve()
          })
          .catch((error) => {
          reject(error)
          })
          .finally(() => {})
          })
          },
          /**

          * @description 重置accessToken、roles、permission等
          * @param {*} { commit, dispatch }
          */
          async resetAll({ dispatch, commit }) {
          await dispatch('setAccessToken', '')
          commit('SET_ROLES', [])
          commit('SET_PERMISSION', [])
          sessionStorage.clear()
          },

          好,至此,整個RBAC的前端權(quán)限方案設(shè)計到實現(xiàn)就已經(jīng)宣告完成,其實還有很多需要優(yōu)化的地方,比如把用戶路由表信息緩存到本地臨時緩存中,這樣用戶刷新的時候就不會因為vuex的特性需要再去請求一邊接口。還比如

             

          js

          復(fù)制代碼

          await store.dispatch('user/GetInfo')
          await store.dispatch('async-router/GenerateRoutes')
          let accessRoutes = store.getters['async-router/addRouters']
          accessRoutes.forEach((route) => {
          //用來判斷是否是外鏈
          if (!isHttp(route.path)) {
          router.addRoute(route) // 動態(tài)添加可訪問路由表
          }
          })

          這段代碼可以抽離出來,單獨(dú)封裝。另外還有側(cè)邊導(dǎo)航欄的處理,這個我沒講,一方面是相對簡單,我們獲取到格式化之后的動態(tài)路由數(shù)據(jù)的同時,其實也就是獲取到了導(dǎo)航欄的菜單信息。 另一方面,每個人的項目頁面布局方式不一樣,導(dǎo)航欄的設(shè)計也會不一樣。在此基礎(chǔ)上,針對性的對數(shù)據(jù)進(jìn)行修飾和改造即可。
          不啰嗦了,我寫的比較匆忙,也有些地方我可能沒有考慮到,看到這里的小伙伴,或者對著文章實踐的小伙伴們,歡迎你們提出自己的意見和看法,我會加以訂正,不足之處,還請海涵~~ 


          瀏覽 25
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  色六月综合 | 麻豆三级片在线播放 | 99精品免费视频在线观看 | 黄色精品在线观看 | 99热精品在线观看首页 |