vue從零搭建一個(gè)前中后臺(tái)權(quán)限管理模板

背景

我司有很多需要進(jìn)行權(quán)限管理的產(chǎn)品蹲缠。其中有一個(gè)產(chǎn)品棺克,需要給多個(gè)客戶部署前中后臺(tái)。在開(kāi)發(fā)第一個(gè)版本時(shí)线定,代碼全部分離娜谊。前端三套,后端三套斤讥。加上kafka纱皆,redis,算法,數(shù)據(jù)庫(kù)等服務(wù)器派草,每有一個(gè)新的客戶就需要部署一次搀缠,需要花費(fèi)很長(zhǎng)的時(shí)間且代碼難以維護(hù)。

后決定重構(gòu)代碼近迁,產(chǎn)品分為前艺普,中,后三個(gè)平臺(tái)鉴竭。前后端分別一套代碼歧譬,支持權(quán)限管理,可拓展搏存。前端使用路由前綴判斷平臺(tái)瑰步,登錄時(shí)會(huì)返回不同的token和用戶信息。不同的token只能訪問(wèn)對(duì)應(yīng)平臺(tái)的接口祭埂,根據(jù)用戶角色生成可訪問(wèn)的菜單面氓,進(jìn)入不同的系統(tǒng)

前言

權(quán)限模塊對(duì)于一個(gè)項(xiàng)目來(lái)說(shuō)是比較麻煩的部分,通常一個(gè)項(xiàng)目的權(quán)限管理蛆橡,需要做的是下面三種級(jí)別的鑒權(quán)舌界。

  1. 平臺(tái)級(jí)別
  2. 頁(yè)面級(jí)別(菜單)
  3. 控件級(jí)別(如按鈕,表格展示字段等)

本篇文章站在前端的角度泰演,實(shí)現(xiàn)前兩種級(jí)別的權(quán)限管理(控件級(jí)別可以通過(guò)條件渲染實(shí)現(xiàn))呻拌。用vue從零搭建一個(gè)前中后臺(tái)權(quán)限管理模板。供大家參考睦焕。

演示地址:http://auth.percywang.top

項(xiàng)目地址:https://github.com/pppercyWang/vue-authentication

其實(shí)大部分項(xiàng)目都會(huì)分離前后臺(tái)藐握,因?yàn)檎显谝惶状a,確實(shí)對(duì)打包優(yōu)化垃喊,代碼分割需要做的更多猾普。且項(xiàng)目架構(gòu)上會(huì)復(fù)雜一些,安全性方面需要考慮的更全面本谜。這里也提供了一個(gè)純后臺(tái)的權(quán)限管理模板初家。

項(xiàng)目地址:https://github.com/pppercyWang/vue-authentication2

項(xiàng)目結(jié)構(gòu)

技術(shù)棧:vue vue-router vuex element

assets  靜態(tài)資源
plugins
    element-style.scss  element樣式
    element.js   按需引入
router
    index.js 靜態(tài)路由及createRouter方法
service
    api.js  前中后臺(tái)接口管理
store  vuex
utils
    http.js axios封裝
views
    foreground  前臺(tái)頁(yè)面
    midground   中臺(tái)頁(yè)面
    background  后臺(tái)頁(yè)面
    layout    前中后臺(tái)布局文件
    404.vue   404頁(yè)面
    Login.vue   前臺(tái)登錄
    AgentLogin.vue   中臺(tái)登錄
    AdminLogin.vue   后臺(tái)登錄
permission.js   動(dòng)態(tài)路由 前中后臺(tái)鑒權(quán) 菜單數(shù)據(jù)生成
main.js  應(yīng)用入口

一. 路由初始化——staticRoutes

三個(gè)平臺(tái)登錄是三個(gè)不一樣的頁(yè)面。/開(kāi)頭的是前臺(tái)的路由乌助,/agent是中臺(tái)溜在,/admin是后臺(tái)。這里的重定向也可以跳轉(zhuǎn)到具體的頁(yè)面他托,但這里因?yàn)闄?quán)限角色不同的原因掖肋,不能寫(xiě)死,就直接重定向到登錄頁(yè)赏参。

注意:404頁(yè)需要放在路由的最后面志笼,所以放在動(dòng)態(tài)路由部分

router/index.js

const staticRoutes = [{
    path: '/login',
    name: '用戶登錄',
    component: () => import('@/views/Login.vue'),
  },
  {
    path: '/agent/login',
    name: '中臺(tái)登錄',
    component: () => import('@/views/AgentLogin.vue'),
  },
  {
    path: '/admin/login',
    name: '后臺(tái)登錄',
    component: () => import('@/views/AdminLogin.vue'),
  },
  {
    path: '/',
    redirect: '/login',
  },
  {
    path: '/agent',
    redirect: '/agent/login',
  },
  {
    path: '/admin',
    redirect: '/admin/login',
  },
]

二. 動(dòng)態(tài)路由——dynamicRoutes

本例只有中臺(tái)和后臺(tái)進(jìn)行鑒權(quán)沿盅,一級(jí)欄目需要icon字段,用于菜單項(xiàng)圖標(biāo)籽腕。children為一級(jí)欄目的子欄目嗡呼,meta中的roles數(shù)組代表可訪問(wèn)該route的角色纸俭。

permission.js

const dynamicRoutes = {
    // 前臺(tái)路由
    'user': [{
        path: '/',
        component: () => import('@/views/layout/Layout.vue'),
        name: '首頁(yè)',
        redirect: '/home',
        children: [{
            path: 'home',
            component: () => import('@/views/foreground/Home.vue'),
        }]
    }, ],
    // 中臺(tái)路由
    'agent': [{
            path: '/agent/member',
            component: () => import('@/views/layout/AgentLayout.vue'),
            name: '會(huì)員管理',
            redirect: '/agent/member/index',
            icon: 'el-icon-star-on',
            children: [{
                    path: 'index',
                    component: () => import('@/views/midground/member/Index.vue'),
                    name: '會(huì)員列表',
                    meta: {
                        roles: ['super_agent', 'second_agent'] // 超級(jí)代理和二級(jí)都可訪問(wèn)
                    },
                },
                {
                    path: 'scheme',
                    component: () => import('@/views/midground/member/Scheme.vue'),
                    name: '優(yōu)惠方案',
                    meta: {
                        roles: ['super_agent']  // 只有超級(jí)代理可訪問(wèn)
                    },
                },
            ]
        },
    ],
    // 后臺(tái)路由
    'admin': [{
            path: '/admin/user',
            component: () => import('@/views/layout/AdminLayout.vue'),
            name: '用戶管理',
            redirect: '/admin/user/index',
            icon: 'el-icon-user-solid',
            children: [{
                    path: 'index',
                    component: () => import('@/views/background/user/Index.vue'),
                    name: '用戶列表',
                    meta: {
                        roles: ['super_admin', 'admin']
                    },
                },
                {
                    path: 'detail',
                    component: () => import('@/views/background/user/UserDetail.vue'),
                    name: '用戶詳情',
                    meta: {
                        roles: ['super_admin']
                    },
                },
            ]
        },
    ],
    '404': {
        path: "*",
        component: () => import('@/views/404.vue'),
    }
}

三. 登錄頁(yè)

通常在登錄成功之后皇耗,后端會(huì)返回token跟用戶信息,我們需要對(duì)token跟用戶信息進(jìn)行持久化揍很,方便使用郎楼,這里我直接存在了sessionStorage。再根據(jù)用戶角色的不同進(jìn)入不同的路由

views/adminLogin.vue

try {
    const res = await this.$http.post(`${this.$api.ADMIN.login}`, this.form.loginModel)
    sessionStorage.setItem("adminToken", res.Data.Token);
    const user = res.Data.User
    sessionStorage.setItem(
        "user",
        JSON.stringify({
            username: user.username,
            role: user.role,
            ground: user.ground // 前中后臺(tái)的標(biāo)識(shí)  如 fore mid back
        })
    );
    switch (user.role) {
        case "ip_admin": // ip管理員
            this.$router.push("/admin/ip/index");
            break;
        case "admin": // 普通管理員
            this.$router.push("/admin/user/index");
            break;
        case "super_admin": // 超級(jí)管理員
            this.$router.push("/admin/user/index");
            break;
    }
} catch (e) {
    this.$message.error(e.Message)
}

四. 路由守衛(wèi)——router.beforeEach()

只要是進(jìn)入登錄頁(yè)窒悔,我們需要做兩個(gè)事呜袁。

  1. 清除存儲(chǔ)在sessionStorage的token信息和用戶信息
  2. 使用permission.js提供的createRouter()創(chuàng)建一個(gè)新的router實(shí)例,替換matcher简珠。

我們這里是使用addRoutes在靜態(tài)路由的基礎(chǔ)上添加新路由阶界,但是文檔中沒(méi)有提供刪除路由的api×郑可以試想一下膘融,如果登錄后臺(tái)再登錄中臺(tái),則會(huì)出現(xiàn)中臺(tái)可以訪問(wèn)后臺(tái)路由的情況祭玉。為什么替換matcher可以刪除addRoutes添加的路由氧映?

注:router.beforeEach一定要放在vue實(shí)例創(chuàng)建之前,不然當(dāng)頁(yè)面刷新時(shí)的路由不會(huì)進(jìn)beforeEach鉤子

main.js

router.beforeEach((to, from, next) => {
  if (to.path === '/login' || to.path === '/agent/login' || to.path === '/admin/login') {
    sessionStorage.clear();
    router.matcher = createRouter().matcher // 初始化routes,移除所有dynamicRoutes
    next()
    return
  }
  authentication(to, from, next, store, router); //路由鑒權(quán)
})
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

五. 前中后臺(tái)鑒權(quán)——authentication()

這里的switch函數(shù)根據(jù)to.path.split("/")[1]判定平臺(tái)脱货。在登錄時(shí)成功后我們sessionStorage.setItem()保存token岛都。
為什么要使用token agentToken adminToken三個(gè)不同的key來(lái)儲(chǔ)存呢?而不是只將token作為key呢振峻。這樣在axios.interceptors.request.use攔截器中設(shè)置token頭也不需要通過(guò)switch去獲取不同的token了臼疫。

因?yàn)榧僭O(shè)我們當(dāng)前的頁(yè)面路由是agent/member/index,我們手動(dòng)修改為admin/xxx/xxx扣孟。我們希望它跳轉(zhuǎn)到admin的登錄頁(yè)烫堤,而不是404頁(yè)面。

isAuthentication標(biāo)識(shí)是否完成鑒權(quán)哈打,沒(méi)有鑒權(quán)則調(diào)用generateRoutes獲取有效路由,再通過(guò)addRoutes添加新路由

permission.js

export function authentication(to, from, next, store, router) {
    let token;
    switch (to.path.split("/")[1]) {
        case 'agent':
            token = sessionStorage.getItem('agentToken');
            if (!token && to.path !== '/agent/login') {
                next({
                    path: '/agent/login'
                })
                return
            }
            break;
        case 'admin':
            token = sessionStorage.getItem('adminToken');
            if (!token && to.path !== '/admin/login') {
                next({
                    path: '/admin/login'
                })
                return
            }
            break;
        default:
            token = sessionStorage.getItem('token');
            if (!token && to.path !== '/login') {
                next({
                    path: '/login'
                })
                return
            }
            break;
    }
    const isAuth = sessionStorage.getItem('isAuthentication')
    if (!isAuth || isAuth === '0') {
        store.dispatch('getValidRoutes', JSON.parse(sessionStorage.getItem('user')).role).then(validRoutes => {
            router.addRoutes(validRoutes)
            sessionStorage.setItem('isAuthentication', '1')
        })
    }
    next();
}

通過(guò)user.ground判定平臺(tái)

store/index.js

   getValidRoutes({commit}, role) {
      return new Promise(resolve => {
        let validRoutes
        switch (JSON.parse(sessionStorage.getItem('user')).ground) {
          case 'fore':
            validRoutes = generateRoutes('user', role, commit)
            resolve(validRoutes);
            break
          case 'mid':
            validRoutes = generateRoutes('agent', role, commit)
            resolve(validRoutes);
            break
          case 'back':
            validRoutes = generateRoutes('admin', role, commit)
            resolve(validRoutes);
            break
        }
      })
    },

六. 角色篩選——ValidRoutes()

這里干了兩件最重要的事

  1. 生成el-menu的菜單數(shù)據(jù)
  2. 生成當(dāng)前角色有效的路由

permission.js

export function generateRoutes(target, role, commit) {
    let targetRoutes = _.cloneDeep(dynamicRoutes[target]);
    targetRoutes.forEach(route => {
        if (route.children && route.children.length !== 0) {
            route.children = route.children.filter(each => {
                if (!each.meta || !each.meta.roles) {
                    return true
                }
                return each.meta.roles.includes(role) === true
            })
        }
    });
    switch (target) {
        case 'admin':
            commit('SET_BACKGROUD_MENU_DATA', targetRoutes.filter(route => route.children && route.children.length !== 0)) // 菜單數(shù)據(jù)是不需要404的
            break
        case 'agent':
            commit('SET_MIDGROUD_MENU_DATA', targetRoutes.filter(route => route.children && route.children.length !== 0))
            break
    }
    return new Array(...targetRoutes, dynamicRoutes['404'])
}

七.頁(yè)面刷新后數(shù)據(jù)丟失

在登錄后isAuthentication為1塔逃,刷新時(shí)不會(huì)重新生成路由,導(dǎo)致數(shù)據(jù)丟失料仗,在main.js監(jiān)聽(tīng)window.onbeforeunload即可

main.js

window.onbeforeunload = function () {
  if (sessionStorage.getItem('user')) {
    sessionStorage.setItem('isAuthentication', '0') // 在某個(gè)系統(tǒng)登錄后湾盗,頁(yè)面刷新,需重新生成路由
  }
}
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

拓展

這時(shí)候差不多就大功告成了立轧,只需將數(shù)據(jù)渲染到el-menu即可格粪。

1.后臺(tái)控制權(quán)限

當(dāng)前的路由鑒權(quán)基本上由前端控制躏吊,后端只需返回平臺(tái)標(biāo)識(shí)和角色。但實(shí)際開(kāi)發(fā)時(shí)帐萎,肯定都是通過(guò)后臺(tái)控制比伏,菜單角色等信息需要建表入庫(kù)。來(lái)修改欄目名稱疆导,一級(jí)欄目icon赁项,菜單權(quán)限等
我們可以在getValidRoutes時(shí)獲取一張權(quán)限表,將這些數(shù)據(jù)插入到dynamicRoutes中澈段。后端返回的數(shù)據(jù)大致如下:

[{
        id: 1,
        name: '用戶管理',
        icon: 'el-icon-user-solid',
        children: [{
                id: 3,
                name: '用戶列表',
                meta: {
                    roles: [1, 2]
                },
            },
            {
                id: 4,
                path: 'detail',
                name: '用戶詳情',
                meta: {
                    roles: [1]
                },
            },
        ]
    },
    {
        id: 2,
        name: 'IP管理',
        icon: 'el-icon-s-promotion',
        children: [{
            id: 5,
            name: 'IP列表',
            meta: {
                roles: [1, 2, 3]
            },
        }, ]
    },
]

2.安全性方面

前端:

  1. 跨平臺(tái)進(jìn)入路由悠菜,直接跳到該平臺(tái)登錄頁(yè)。
  2. 當(dāng)前平臺(tái)訪問(wèn)沒(méi)有權(quán)限的頁(yè)面報(bào)404錯(cuò)誤败富。

后端:

  1. 一定要保證相應(yīng)平臺(tái)的token只能調(diào)對(duì)應(yīng)接口悔醋,否則報(bào)錯(cuò)。
  2. 如果能做到角色接口鑒權(quán)就更好了兽叮,從接口層面拒絕請(qǐng)求

3.axios封裝

在請(qǐng)求攔截器中根據(jù)用戶信息拿不同的token芬骄,設(shè)置頭部信息
在響應(yīng)攔截器中,如果token過(guò)期鹦聪,再根據(jù)用戶信息跳轉(zhuǎn)到不同的登錄頁(yè)

4.api管理

如果后端也是一套代碼账阻。那api也可以這樣進(jìn)行管理,但如果沒(méi)有一個(gè)統(tǒng)一的前綴椎麦≡咨可以在axios設(shè)置一個(gè)統(tǒng)一的前綴例如proxy,這樣就解決了跨域的問(wèn)題观挎。

const USER = 'api'
const AGENT = 'agent'
const ADMIN = 'admin'
export default {
  USER: {
    login: `${USER}/User/login`,
  },
  AGENT: {
    login: `${AGENT}/User/login`,
    uploadFile: `${AGENT}/Utils/uploadFile`,
  },
  ADMIN: {
    login: `${ADMIN}/User/login`,
  },
}
devServer: {
    proxy: {
      '/proxy': {
        target: 'http://localhost:8848',
        changeOrigin: true,
        pathRewrite: {
          '^proxy': ''  //將url中的proxy子串去掉
        }
      }
    }
  },
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末琴儿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嘁捷,更是在濱河造成了極大的恐慌造成,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雄嚣,死亡現(xiàn)場(chǎng)離奇詭異晒屎,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)缓升,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)鼓鲁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人港谊,你說(shuō)我怎么就攤上這事骇吭。” “怎么了歧寺?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,875評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵燥狰,是天一觀的道長(zhǎng)棘脐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)龙致,這世上最難降的妖魔是什么蛀缝? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,441評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮目代,結(jié)果婚禮上屈梁,老公的妹妹穿的比我還像新娘。我一直安慰自己像啼,他們只是感情好俘闯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布潭苞。 她就那樣靜靜地躺著忽冻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪此疹。 梳的紋絲不亂的頭發(fā)上僧诚,一...
    開(kāi)封第一講書(shū)人閱讀 51,365評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音蝗碎,去河邊找鬼湖笨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛蹦骑,可吹牛的內(nèi)容都是我干的慈省。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼眠菇,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼边败!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起捎废,我...
    開(kāi)封第一講書(shū)人閱讀 39,062評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤笑窜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后登疗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體排截,經(jīng)...
    沈念sama閱讀 45,500評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評(píng)論 3 335
  • 正文 我和宋清朗相戀三年辐益,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了断傲。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡智政,死狀恐怖认罩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情女仰,我是刑警寧澤猜年,帶...
    沈念sama閱讀 35,559評(píng)論 5 345
  • 正文 年R本政府宣布抡锈,位于F島的核電站,受9級(jí)特大地震影響乔外,放射性物質(zhì)發(fā)生泄漏床三。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評(píng)論 3 328
  • 文/蒙蒙 一杨幼、第九天 我趴在偏房一處隱蔽的房頂上張望撇簿。 院中可真熱鬧,春花似錦差购、人聲如沸四瘫。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,779評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)找蜜。三九已至,卻和暖如春稳析,著一層夾襖步出監(jiān)牢的瞬間洗做,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,912評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工彰居, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诚纸,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評(píng)論 2 370
  • 正文 我出身青樓陈惰,卻偏偏與公主長(zhǎng)得像畦徘,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抬闯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容