系統權限按需訪問路由幾個完整方案(含addRoutes的填坑)

前言

當你的系統須要作權限驗證時,每每有一個很常見的需求:系統的某些頁面或者資源(按鈕、操做等),須要該用戶有對應的權限才能可見可用。javascript

這就涉及到如何根據用戶的權限來判斷可否進入某個路由頁面的問題了。vue

網上有不少零散的方案,並無橫向對比幾種方案,且不少細節沒解釋到位,此處提供完整的幾個方案流程,並總結優缺點,你可自行選擇java

本篇是針對vue-router來講明如何實現。git

解決方案

根據各類資料,這裏分爲三種解決方案來分別描述其優缺點。github

beforeEach中限制

你能夠註冊所有路由,在router.beforeEach中即進入路由前進行判斷,即將進入的路由是有權限進入,不能的話手動重定向到某個靜態路由(不須要權限就能進入的頁面,即任何用戶都能進入的頁面,如404頁或首頁)vue-router

因爲每一個系統的權限方案不同,判斷條件也不同,這裏就僅僅簡單舉個例子,萬變不離其宗,但願你們觸類旁通,舉一反三。vuex

咱們在router.beforeEach判斷是否有權限進入,須要有三點:api

  1. 在路由配置中作標識,告知該路由須要的權限
  2. 須要一處地方記錄該用戶所擁有的權限信息
  3. router.beforeEach結合第1點和第2點進行判斷

1)路由配置中作標識

假設項目的權限是用ID來表示,即每一個權限,用一個ID值來表示。數組

我採用路由配置的props項來作標識,authorityId值表示權限對應的ID值。bash

import Vue from 'vue';
import Router from 'vue-router';

import home from 'home.vue';
import exam1 from 'example1.vue';
import exam2 from 'example2.vue';

Vue.use(Router);

const routes = [
    {
        path: '/',
        component: home
    },
    {
        path: '/exam1',
        component: exam1,
        props: {
            authorityId: 100
        }
    },
    {
        path: '/exam2',
        component: exam2,
        props: {
            authorityId: 200
        }
    }
];

const router = new Router({
    routes
});

export default router;
複製代碼

上面是設置路由的主文件,從中咱們看到,兩個頁面分別有兩個不一樣的權限ID值,要可以進入頁面,就得擁有這兩個權限。

若是看過個人這篇文章 如何寫出一個利於擴展的vue路由配置 ,就知道我喜歡按照功能模塊來把路由配置細分不少個模塊,若是你是按功能模塊區分權限,即一個功能模塊下好多個頁面都是一個權限ID,那麼能夠在routes數組的最後統一加上authorityId,而不用一個個都寫,累贅!

routes.forEach(item => {
    item.props = {
        ...item.props,
        authorityId: 100
    };
});
複製代碼

2)存儲權限信息

接着咱們要找個地方來存儲一下用戶所擁有的權限信息,若是你對用戶的權限信息是保存在持久化的一個地方如sessionStorage、localStorage、cookie或url中的話,刷新後還能繼續能拿到這些值,那麼再根據這些值控制路由訪問,這是沒多大問題的。可是,這種重要的信息就暴露在外面?萬一別人噁心修改了,把本身不能訪問的權限改爲能夠訪問呢?

所以上述方法是不建議的。

我通常會存在vuex中,那麼存在這裏的話,就會面臨刷新頁面了,vuex的信息也會丟失的問題。

爲了解決這個問題,咱們一樣須要保存一些信息到持久化的一個地方中,可是與上面不一樣的是,咱們不要直接保存權限信息,而是保存一些能發請求獲取權限信息的信息,常見的如用戶id等。刷新後,根據保存的這些信息發請求從新獲取權限信息並存儲。

如這裏的例子我就設置sessionStorage.setItem('userId', 1012313);

如下爲存儲權限信息的vuex內容:

// authority.js

import * as types from '../mutation-types';

// state
const state = {
    // 權限id值數組,null爲初始化狀況,若是爲[]表明該用戶沒有任何權限
    rights: null
};

// getters
const getters = {
    rights: state => state.rights
};

// actions
const actions = {
    /** * 設置用戶訪問權限 */
    setRights ({ commit }, value) {
        commit(types.SET_RIGHTS, value);
    }
};

// mutations
const mutations = {
    [types.SET_RIGHTS] (state, value) {
        state.rights = value;
    }
};

export default {
    state,
    getters,
    actions,
    mutations
};
複製代碼

這裏值得一提的是,爲何rights默認值是null而不是[],緣由是用來區分是初始化狀態仍是真的無任何權限狀態。這個有使用場景,特別是針對刷新頁面。

就是當你目前在一個非權限路由頁面上時,若是你刷新了頁面,用戶的鑑權還有效,理應仍是停留在這個動態路由的頁面。

那你怎麼判斷如今是因爲刷新了頁面呢,就是經過判斷rightsnull而不是[],若是rights初始值自己就是[]的話,這是沒法判斷出來的。

那麼爲null是有兩種狀況的:

  • 從空tab或別的網站進入到你的網站(如輸入url、sso登陸跳轉過來);
  • 刷新頁面

因此爲了進一步區分是刷新行爲,則需進一步經過判斷sessionStorage裏有沒有登錄後存儲的userId信息,由於若是userId存在了表明登陸了,登陸了就會進行權限的設置,就天然rights會有值,就算沒權限也會是個[]

上面討論的這些判斷行爲,都會在router.beforeEach中體現應用到。

3)判斷是否有權限進入路由

仍是在路由主文件中,在全局前置守衛中作判斷。

import store from '/store';

/** * 檢查進入的路由是否須要權限控制 * @param {Object} to - 即將進入的路由對象 * @param {Object} from - 來自的路由對象 * @param {Function} next - 路由跳轉的函數 */
const verifyRouteAuthority = async (to, from, next) => {
    // 獲取路由的props下的authorityId信息
    const defaultConfig = to.matched[to.matched.length - 1].props.default;
    const authorityId = (defaultConfig && defaultConfig.authorityId) ? defaultConfig.authorityId : null;

    // authorityId存在,表示須要權限控制的頁面
    if (authorityId) {
        // 獲取vuex中存儲權限信息的模塊,authority爲該模塊名
        const authorityState = store.state.authority;
        // 爲null的場景: 從空tab或別的網站進入到eod(如輸入url、sso登陸跳轉過來);刷新頁面;
        if (authorityState.rights === null) {
            const userId = sessionStorage.getItem('userId');
            // 若是是刷新了致使存儲的權限路由配置信息沒了,則要從新請求獲取權限,判斷刷新頁是否擁有權限
            if (userId) {
                // 從新獲取權限,如下爲例子
                const res = await loginService.getRights();
                store.dispatch('setRights', res);
            } else { // 若是是非當頁刷新,則跳轉到首頁
                next({ path: '/' });
                return true;
            }
        }

        // 若是是要進行權限控制的頁面,判斷是否有對應權限,無則跳轉到首頁
        if (!authorityState.rights.includes(authorityId)) {
            next({ path: '/' });
            return true;
        }
    }

    return false;
};

/** * 能進入路由頁面的處理 */
const enterRoute = async (to, from, next) => {
    // 進行權限控制校驗
    const res = await verifyRouteAuthority(to, from, next);
    // 若是通不過檢驗已進行內部跳轉,則退出該流程
    if (res) {
        return;
    }

    // 進行登陸驗證以及獲取必要的用戶信息等操做
    // ...
};

router.beforeEach((to, from, next) => {
    // 無匹配路由
    if (to.matched.length === 0) {
        // 跳轉到首頁 添加query,避免手動跳轉丟失參數,例如token
        next({
            path: '/',
            query: to.query
        });
        return;
    }
    enterRoute(to, from, next);
});
複製代碼

4)退出清空權限信息

完整的一個方案,別忘了還要針對登出,清空權限信息這步。也很簡單,清空,意味着把rights從新置爲null,所以執行store.dispatch('setRights', null);便可

小結

  • 優勢:對於註冊路由的處理沒有額外的操做,全部處理邏輯集中在router.beforeEach中判斷
  • 缺點:註冊了多餘的路由(但彷佛,沒啥關係?)

刷新頁面從新註冊路由

這是一個極其簡單粗暴的方式:

在網站vue app實例化時,router也初始化了,這時候只註冊了靜態路由(如登陸頁、404頁等不須要權限的頁面),當用戶登陸了以後,拿到用戶的權限的接口,把這些權限的信息儲存在某個持久化的地方如sessionStorage、cookie甚至url中,而後手動刷新頁面location.reload,在建立路由實例時,拿到剛存的權限信息,而後才建立新的路由實例。

可能有人會問,爲何不像上一個方案說的,只存儲如userId這樣的信息而不是直接存權限信息。若是存了userId,再經過請求獲取權限信息,這是一個異步的過程,網站vue app實例化時,router也初始化了,很難找到一個時機在router初始化前就拿到權限的信息。

因爲這種方式十分簡單粗暴,我我的不喜歡,體驗也很差,因此我僅提供思路,具體實現就不寫代碼了。

  • 缺點:容易泄露權限信息,便於別人惡意篡改,除非你能夠作什麼加密處理把,可是還要解密,挺麻煩的;多刷新了一次,用戶體驗很差。

addRoutes動態註冊路由

目前vue-router 3.0要實現動態路由(即視狀況註冊路由),僅僅提供addRoutes一個api,在官方github中也有許多人提issue但願新增一些其餘實現動態路由的功能,如刪除已註冊路由替換同名路由等,可是維護者的回覆大概意思是目前vue-router是以靜態路由爲主而設計的,不可能一會兒就考慮很全面,一步登天,給點時間後面在慢慢完善。

下面就說,在如此背景下,如何利用addRoutes來實現動態路由,以知足權限的變更

addRoutes函數說白了,就是用來追加路由註冊的。最簡單的思路是,當用戶登陸到系統後,就根據用戶的權限來追加註冊TA能訪問的路由。

可是,一套完整的方案,會有如下幾個方面你須要考慮的:

  • 1)切換用戶後,權限發生變化,註冊的路由也應該要變化,理想狀況是刪除已註冊的動態路由,而後才從新追加新路由。
  • 2)刷新頁面時,若是用戶鑑權還經過,那麼其權限所容許的頁面應該還能繼續訪問
  • 3)登出系統,即用戶退出,須要清除已註冊路由

針對問題一

上面也說了,目前vue-router不提供刪除已註冊路由的api,只有一個addRoutes能夠動態改變註冊路由,其接受一個參數,是個路由配置的數組。

那麼若是不作處理,直接採用addRoutes追加註冊,就會可能發生追加劇復路由的狀況

例如用戶1擁有 a,b 權限,用戶2擁有 a,c 權限。當用戶1登陸上了,此時路由已註冊 a,b 權限對應的路由,而後用戶1退出切換到用戶2,經過addRoutes把 a,c 權限對應的路由追加註冊了,這時候,就會重複註冊了a路由,在控制檯中會有警告信息。

其實若是路由都是徹底同樣的話,不會影響到實際應用,用戶也無是無感知的,只是路由變得累贅。可是若是假設同name的路由倒是對應不一樣的頁面路徑,這時候我就會有問題了。

若是你知道存在有同名name路由,存在什麼隱形後果,請告訴我。

所以,咱們須要找一個方案,解決可能添加劇復路由的問題。

有很多資料會讓你在切換用戶時,在跳轉到登陸界面時,刷新一下頁面,就會變回整個網站初始化的狀況,即路由也從新初始化實例,這樣登陸後就再用addRoutes追加路由就好。

其實上述方案不失爲一個好方案,若是你不介意會刷新一下頁面的話。甚至你的登陸界面就是跟系統不在一個單頁面應用的話就更加不用手動刷新了(若有專門的單點登陸平臺),天然就能在登陸後從新進入系統初始化了。

要說缺點的話:

  • 要從新刷新頁面,若是系統網站自己初始化加載很慢的話,那麼用戶體驗不好。
  • 若是你的系統權限方面比較複雜,像我開發的系統,權限不只僅在用戶之間,在用戶裏,不一樣任務下也有不一樣權限,這時,就不能用這種方式了,由於切換任務並不會要從新登陸

若是你不喜歡上面這個簡單的方案的話,不妨繼續往下看

import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);

// 建立路由實例的函數
// 這裏的staticRoutes表示你係統的靜態路由
const createRouter = () => {
    return new Router({
        routes: staticRoutes
    });
};
/** * 重置註冊的路由導航map * 主要是爲了經過addRoutes方法動態注入新路由時,避免重複註冊相同name路由 */
const resetRouter = () => {
    const newRouter = createRouter();
    router && (router.matcher = newRouter.matcher);
};

// 這是伴隨vue app實例化的初始化路由實例
const router = createRouter();

export { resetRouter };
export default router;
複製代碼

上面是建立路由的一份代碼,除了resetRouter,其他部分跟你本來建立路由的代碼並沒有什麼不一樣。而resetRouter的做用就是解決重複問題的關鍵(router.matcher = newRouter.matcher),這句至關於重置了路由映射關係,抹去了已註冊的路由映射關係,跟新的路由實例的映射同樣。

所以,在每次經過addRoutes追加註冊路由前,都要使用resetRouter方法來重置一下路由映射,再追加。可是這樣仍然是不能百分百避免重複問題,爲何呢?

以上述代碼爲例子,若是staticRoutes中有一個路由是擁有children子路由的,如

{
    path: '/tsp',
    name: 'TSP',
    component: TSP,
    children: [
        {
            path: 'analysis',
            name: 'analysis',
            component: Analysis
        }
    ]
}
複製代碼

而後你要追加的路由恰好就是在這children中的子路由的話,你就須要追加整個nameTSP的路由了,這時就會發生重複追加已存在的TSPAnalysis的路由了。

爲了不該問題,人爲的約定,靜態路由staticRoutes中不能是有可能被追加路由(包含子孫路由)的路由。

真要發生上面要追加在子路由的狀況,那麼把該TSP路由在初始化路由實例後,而後手動追加一次,僞裝是靜態路由,這樣在使用resetRouter重置後就不包含TSP路由了,而後再追加這個TSP路由就不會警告重複了。

針對問題二

這個問題,咱們在第一個方案中也說過了,關於刷新帶來的問題以及思考。

思路咱們有了,那麼在代碼的具體什麼時機進行操做呢?因爲須要進行異步請求,因此不適宜在路由實例初始化時進行,咱們在beforeEach中作處理,如下爲例子(具體說明是註釋中):

先看vuex中定義存儲權限信息的關鍵代碼

// authority.js

const state = {
    functionModules: null, // 功能模塊權限id值數組,null爲初始化狀況,若是爲[]表明該用戶沒有任何權限
};

const getters = {
    functionModules: state => state.functionModules
};

const actions = {
    /** * 設置用戶所擁有的的功能模塊訪問權限 */
    setFunctionModules ({ commit, state }, value) {
        // ... 這裏省略了實現代碼,由於此節重點不在這,後面再詳說
    }
};

const mutations = {
    // 設置用戶所擁有的的功能模塊訪問權限
    [types.SET_FUNCTION_MODULES] (state, value) {
        state.functionModules = value;
    }
};
複製代碼

下面是在beforeEach的處理邏輯

router.beforeEach((to, from, next) => {
    // 判斷是否有匹配路由
    // 因爲刷新了頁面,路由從新初始化,只有靜態路由被註冊了,
    // 因此進入這個動態路由頁面時,是找不到路由匹配項的
    if (to.matched.length === 0) {
        // 獲取存儲的用來獲取權限信息的信息
        const userId = sessionStorage.getItem('userId');
        // 若是是刷新了致使存儲的權限路由配置信息沒了,則要從新請求獲取權限,判斷刷新頁是否擁有權限
        // 這裏的store.state.authority.functionModules是vuex中存在權限信息的state,是個數組
        // 刷新頁面會變回初始值,例子中是null
        // 這個條件判斷的目的是區分 1.用戶胡亂輸入根本不會存在的路由 2. 在某個動態路由上刷新了頁面
        // functionModules爲null,且保存了userId就表明是第二種狀況,
        // 由於若是userId存在了表明登陸了,就天然functionModules會有值,就算沒權限也會是個[]
        if (store.state.authority.functionModules === null && userId) {
            // 從新獲取權限,如下爲例子
            http.get('/rights').then(res => {
                // vuex中用於保存權限信息的action
                store.dispatch('setFunctionModules', res);
                router.replace(to);
            });
            return;
        }
        // 跳轉到首頁 添加query,避免手動跳轉丟失參數,例如token
        next({
            path: '/',
            query: to.query
        });
        return;
    }
    // ... 其他的一些有匹配路由的操做
});
複製代碼

針對問題三

登出系統,即用戶退出,須要清除已註冊的動態路由。因爲問題二的解決,也須要清除在vuex中的存儲信息。

這個問題其實沒啥難度的,清空動態路由,用上述的resetRouter便可,清空vuex的信息就置爲初始值就。

我爲啥這裏一提,就是爲了提示你還有這麼一個流程,別忘記了,一整套完整的方案不能漏了這個。

addRoutes的缺陷

上述基本已經描述完一整套實現動態路由的解決方案。可是有些小細節,能夠注意一下,提升方案的全面性。

關於addRoutes的詳細解釋,官方文檔也是簡單一筆帶過,實際動態注入路由是怎麼一回事,你會不會以爲注入後,咱們寫配置裏的routes選項值,就是添加了咱們追加的內容?很遺憾,並非這樣的。

咱們在控制檯上打印路由實例router,能夠看到其下有個options屬性,裏面有個routes屬性。這個就是咱們建立路由實例時的routes選項內容。咱們覺得經過addRoutes動態註冊路由後,新註冊的內容也會出如今這個屬性裏,但結果倒是沒有。

$router.options.routes的內容只會是在建立實例時生成,後面追加的不會出如今這裏。這意味着,在這個版本下的vue-router你無法經過路由實例對象來獲知當前已註冊的全部路由。假設你的系統有須要利用固然已註冊的全部路由來轉一些處理的話,你此時就沒有這個數據了。所以,咱們要本身作一個備份,記錄當前已註冊的路由,以防不時之需。

咱們在剛纔的vuex文件中存儲這個已註冊路由信息,並補充具體的setFunctionModules邏輯

// authority.js

import staticRoutes from '@/router/staticRoutes.js';

// 因爲vuex的檢查機制,不容許存在在mutation外部能改變state值的可能性(特別是賦值類型是數組或對象時),因此要深拷貝一下
const _staticRoutes = JSON.parse(JSON.stringify(staticRoutes));

const state = {
    functionModules: null,
    // 當前已註冊的路由,由於經過addRoutes追加的路由不會更新到router對象上,須要本身作記錄,以避免不時之需
    // _staticRoutes爲系統的靜止路由
    registeredRoutes: _staticRoutes
};

const getters = {
    functionModules: state => state.functionModules,
    registeredRoutes: state => state.registeredRoutes
};

const actions = {
    /** * 設置用戶所擁有的的功能模塊訪問權限 */
    setFunctionModules ({ commit, state }, value) {
        // 若是和舊值同樣,那麼就不需從新註冊路由
        // 這裏舉例的系統的權限信息是由一個個權限id組成的數組,因此用如下邏輯判斷是否重複,具體項目具體實現
        if (state.functionModules) {
            const _functionModules = state.functionModules.concat();
            _functionModules.sort(Vue.common.numCompare);
            value.sort(Vue.common.numCompare);
            if (_functionModules.toString() === value.toString()) {
                return;
            }
        }
        // 若是沒有任何權限
        if (value.length === 0) {
            resetRouter(); // 重置路由映射
            return;
        }
        // 根據權限信息生成動態路由配置
        // createRoutes函數不展開說明,具體項目具體實現
        const dynamicRoutes = createRoutes();
        resetRouter(); // 重置路由映射
        router.addRoutes(dynamicRoutes); // 追加權限路由
         // 因爲vuex的檢查機制,不容許存在在mutation外部能改變state值的可能性(特別是賦值類型是數組或對象時),因此要深拷貝一下
        const _dynamicRoutes = JSON.parse(JSON.stringify(dynamicRoutes));
        // 記錄當前已註冊的路由配置
        commit(types.SET_REGISTERED_ROUTES, [..._staticRoutes, ..._dynamicRoutes]);
        // 保存權限信息
        commit(types.SET_FUNCTION_MODULES, value);
    }
};

const mutations = {
    // 生成當前已註冊的路由副本
    [types.SET_REGISTERED_ROUTES] (state, value) {
        state.registeredRoutes = value;
    },
    // 設置用戶所擁有的的功能模塊訪問權限
    [types.SET_FUNCTION_MODULES] (state, value) {
        state.functionModules = value;
    }
};

export default {
    state,
    getters,
    actions,
    mutations
};
複製代碼

對了,若是在VUEX中存儲了當前註冊路由信息的話,在問題三中,退出登陸,也要清除這個信息,把它置爲默認狀況,即只有靜態路由的狀況。

// 重置已註冊的路由副本
[types.RESET_REGISTERED_ROUTES] (state) {
    state.registeredRoutes = _staticRoutes;
}
複製代碼

還有一點可能須要知道:

若是經過addRoutes加入的新路由有在靜態路由中的某個路由children中,那麼$router.options.routes會更新上去。

小結

以上即爲一個完整的動態加載路由的方案,這個方案中要注意的東西,要處理好的細節,都已一一說明了。

總結

三個方案都已經說明了,優缺點你們也能知道。沒有說哪一個方案更好,甚至最好的方案,選擇的標準就是:能知足你項目需求的,在你接受缺陷範圍內的最簡單的方案 ,這就是對你來講最好的方案。

若是對你有幫助,可點贊支持下。

未經容許,請勿私自轉載

相關文章
相關標籤/搜索