vue權限管理實現流程

1、總體思路

後端返回用戶權限,前端根據用戶權限處理獲得左側菜單;全部路由在前端定義好,根據後端返回的用戶權限篩選出須要掛載的路由,而後使用 addRoutes 動態掛載路由。javascript

2、實現要點

(1)路由定義,分爲初始路由和動態路由,通常來講初始路由只有 login,其餘路由都掛載在 home 路由之下須要動態掛載。
(2)用戶登陸,登陸成功以後獲得 token,保存在 sessionStorage,跳轉到 home,此時會進入路由攔截根據 token 獲取用戶權限列表。
(3)全局路由攔截,根據當前用戶有沒有 token 和 權限列表進行相應的判斷和跳轉,當沒有 token 時跳到 login,當有 token 而沒有權限列表時去發請求獲取權限等等邏輯。
(4)處理用戶權限,在 store.js 定義一個模塊 permission.js,專門用於處理用戶權限相關的邏輯,用戶權限列表、菜單列表都保存在此模塊;
(5)用戶權限列表、菜單列表的處理,前端的路由要和後端返回的權限有一個惟一標識(通常用路由名作標識符),根據此標識篩選出對應的路由。
(6)左側菜單,要和用戶信息、用戶管理模塊使用的菜單信息一致,統一使用保存在 store 中的變量。前端

3、具體實現流程

一、準備工做,路由定義

/* 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 裏面vue

/* 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',
        }
    },
    // ...
];

二、用戶登陸

用戶進入登陸頁,輸入用戶名、密碼、驗證碼,點擊登陸,發送登陸請求,登陸成功以後,將 token 保存在 sessionStorage,而後跳轉到首頁 /home ,進入路由攔截的邏輯。java

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

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

三、全局路由攔截

首先從打開本地服務 http://localhost:2001 開始,打開後會進入 login 頁面,那麼判斷的依據是什麼?
首先是 token。沒有登陸的用戶是拿不到 token 的,而登陸後的用戶咱們會將 token 存到 seesionStorage,所以,根據當前有沒有 token 便可知道是否登陸。vue-router

/* 全局路由攔截 */
router.beforeEach((to, from, next) => {
    // 根據有沒有token判斷是否登陸
    if (!sessionStorage.token) {
        // 一、當用戶打開localhost,to.matched === [],匹配的是空路由,此時須要重定向到login
        // 二、重定向到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 跳轉到登陸頁。其餘的邏輯就跟具體業務相關了,就不細講了。vuex

四、處理用戶權限

處理用戶權限,在 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 格式大體以下:api

[
  {
    "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",
        "moduleGroupId": 1100,
      },
    ],
  }
]

(2)而後,根據咱們寫好的路由數組,進行對比,過濾獲得咱們要的路由。路由格式在上文「路由定義」的 router/router.js 已經提到。還要根據用戶權限處理獲得側邊欄菜單。數組

爲此,咱們須要兩個處理函數,一個根據用戶權限列表和路由數組過濾獲得最終路由,另外一個根據用戶權限處理獲得側邊欄菜單。因此另外專門建立了一個文件 handle-module.js 存放這兩個函數。session

/* 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: '/',
        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'),
            },
        ]
}

(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 中存儲的各類信息,刷新頁面是最保險的。

相關文件的目錄截圖:
image.png

4、總結

缺點:
全局路由守衛裏,每次路由跳轉都要作判斷;
每次刷新頁面,須要從新發請求獲取用戶權限;
退出登陸時,須要刷新一次頁面將動態添加的路由以及權限信息清空;

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

vue權限管理還有其餘實現方式,你們能夠根據實際業務考慮作調整,以上的實現方式是比較適合咱們現有項目的需求的。以上,有問題歡迎提出交流,喜歡的話點個贊哦~

相關文章
相關標籤/搜索