vue從零搭建一個前中後臺權限管理模板

背景

我司有不少須要進行權限管理的產品。其中有一個產品,須要給多個客戶部署前中後臺。在開發第一個版本時,代碼所有分離。前端三套,後端三套。加上kafka,redis,算法,數據庫等服務器,每有一個新的客戶就須要部署一次,須要花費很長的時間且代碼難以維護。javascript

後決定重構代碼,產品分爲前,中,後三個平臺。先後端分別一套代碼,支持權限管理,可拓展。前端使用路由前綴判斷平臺,登陸時會返回不一樣的token和用戶信息。不一樣的token只能訪問對應平臺的接口,根據用戶角色生成可訪問的菜單,進入不一樣的系統css

前言

權限模塊對於一個項目來講是比較麻煩的部分,一般一個項目的權限管理,須要作的是下面三種級別的鑑權。前端

  1. 平臺級別
  2. 頁面級別(菜單)
  3. 控件級別(如按鈕,表格展現字段等)

本篇文章站在前端的角度,實現前兩種級別的權限管理(控件級別能夠經過條件渲染實現)。用vue從零搭建一個前中後臺權限管理模板。供你們參考。vue

演示地址:auth.percywang.topjava

項目地址:github.com/pppercyWang…ios

其實大部分項目都會分離先後臺,由於整合在一套代碼,確實對打包優化,代碼分割須要作的更多。且項目架構上會複雜一些,安全性方面須要考慮的更全面。這裏也提供了一個純後臺的權限管理模板。git

項目地址:github.com/pppercyWang…github

項目結構

技術棧:vue vue-router vuex elementredis

assets  靜態資源
plugins
	element-style.scss  element樣式
    element.js   按需引入
router
	index.js 靜態路由及createRouter方法
service
    api.js  前中後臺接口管理
store  vuex
utils
	http.js axios封裝
views
	foreground  前臺頁面
    midground   中臺頁面
	background  後臺頁面
    layout    前中後臺佈局文件
    404.vue   404頁面
    Login.vue   前臺登陸
    AgentLogin.vue   中臺登陸
    AdminLogin.vue   後臺登陸
permission.js   動態路由 前中後臺鑑權 菜單數據生成
main.js  應用入口
複製代碼

一. 路由初始化——staticRoutes

三個平臺登陸是三個不同的頁面。/開頭的是前臺的路由,/agent是中臺,/admin是後臺。這裏的重定向也能夠跳轉到具體的頁面,但這裏由於權限角色不一樣的緣由,不能寫死,就直接重定向到登陸頁。算法

注意:404頁須要放在路由的最後面,因此放在動態路由部分

router/index.js

const staticRoutes = [{
    path: '/login',
    name: '用戶登陸',
    component: () => import('@/views/Login.vue'),
  },
  {
    path: '/agent/login',
    name: '中臺登陸',
    component: () => import('@/views/AgentLogin.vue'),
  },
  {
    path: '/admin/login',
    name: '後臺登陸',
    component: () => import('@/views/AdminLogin.vue'),
  },
  {
    path: '/',
    redirect: '/login',
  },
  {
    path: '/agent',
    redirect: '/agent/login',
  },
  {
    path: '/admin',
    redirect: '/admin/login',
  },
]
複製代碼

二. 動態路由——dynamicRoutes

本例只有中臺和後臺進行鑑權,一級欄目須要icon字段,用於菜單項圖標。children爲一級欄目的子欄目,meta中的roles數組表明可訪問該route的角色。

permission.js

const dynamicRoutes = {
    // 前臺路由
    'user': [{
        path: '/',
        component: () => import('@/views/layout/Layout.vue'),
        name: '首頁',
        redirect: '/home',
        children: [{
            path: 'home',
            component: () => import('@/views/foreground/Home.vue'),
        }]
    }, ],
    // 中臺路由
    'agent': [{
            path: '/agent/member',
            component: () => import('@/views/layout/AgentLayout.vue'),
            name: '會員管理',
            redirect: '/agent/member/index',
            icon: 'el-icon-star-on',
            children: [{
                    path: 'index',
                    component: () => import('@/views/midground/member/Index.vue'),
                    name: '會員列表',
                    meta: {
                        roles: ['super_agent', 'second_agent'] // 超級代理和二級均可訪問
                    },
                },
                {
                    path: 'scheme',
                    component: () => import('@/views/midground/member/Scheme.vue'),
                    name: '優惠方案',
                    meta: {
                        roles: ['super_agent']  // 只有超級代理可訪問
                    },
                },
            ]
        },
    ],
    // 後臺路由
    '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'),
    }
}
複製代碼

三. 登陸頁

一般在登陸成功以後,後端會返回token跟用戶信息,咱們須要對token跟用戶信息進行持久化,方便使用,這裏我直接存在了sessionStorage。再根據用戶角色的不一樣進入不一樣的路由

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 // 前中後臺的標識  如 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": // 超級管理員
            this.$router.push("/admin/user/index");
            break;
    }
} catch (e) {
    this.$message.error(e.Message)
}
複製代碼

四. 路由守衛——router.beforeEach()

只要是進入登陸頁,咱們須要作兩個事。

  1. 清除存儲在sessionStorage的token信息和用戶信息
  2. 使用permission.js提供的createRouter()建立一個新的router實例,替換matcher。

咱們這裏是使用addRoutes在靜態路由的基礎上添加新路由,可是文檔中沒有提供刪除路由的api。能夠試想一下,若是登陸後臺再登陸中臺,則會出現中臺能夠訪問後臺路由的狀況。爲何替換matcher能夠刪除addRoutes添加的路由?

注:router.beforeEach必定要放在vue實例建立以前,否則當頁面刷新時的路由不會進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); //路由鑑權
})
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
複製代碼

五. 前中後臺鑑權——authentication()

這裏的switch函數根據to.path.split("/")[1]斷定平臺。在登陸時成功後咱們sessionStorage.setItem()保存token。 爲何要使用token agentToken adminToken三個不一樣的key來儲存呢?而不是隻將token做爲key呢。這樣在axios.interceptors.request.use攔截器中設置token頭也不須要經過switch去獲取不一樣的token了。

由於假設咱們當前的頁面路由是agent/member/index,咱們手動修改成admin/xxx/xxx。咱們但願它跳轉到admin的登陸頁,而不是404頁面。

isAuthentication標識是否完成鑑權,沒有鑑權則調用generateRoutes獲取有效路由,再經過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();
}
複製代碼

經過user.ground斷定平臺

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
        }
      })
    },
複製代碼

六. 角色篩選——generateRoutes()

這裏幹了兩件最重要的事

  1. 生成el-menu的菜單數據
  2. 生成當前角色有效的路由

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)) // 菜單數據是不須要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'])
}
複製代碼

七.頁面刷新後數據丟失

在登陸後isAuthentication爲1,刷新時不會從新生成路由,致使數據丟失,在main.js監聽window.onbeforeunload便可

main.js

window.onbeforeunload = function () {
  if (sessionStorage.getItem('user')) {
    sessionStorage.setItem('isAuthentication', '0') // 在某個系統登陸後,頁面刷新,需從新生成路由
  }
}
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
複製代碼

拓展

這時候差很少就大功告成了,只需將數據渲染到el-menu便可。

1.後臺控制權限

當前的路由鑑權基本上由前端控制,後端只需返回平臺標識和角色。但實際開發時,確定都是經過後臺控制,菜單角色等信息須要建表入庫。來修改欄目名稱,一級欄目icon,菜單權限等 咱們能夠在getValidRoutes時獲取一張權限表,將這些數據插入到dynamicRoutes中。後端返回的數據大體以下:

[{
        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. 跨平臺進入路由,直接跳到該平臺登陸頁。
  2. 當前平臺訪問沒有權限的頁面報404錯誤。

後端:

  1. 必定要保證相應平臺的token只能調對應接口,不然報錯。
  2. 若是能作到角色接口鑑權就更好了,從接口層面拒絕請求

3.axios封裝

在請求攔截器中根據用戶信息拿不一樣的token,設置頭部信息 在響應攔截器中,若是token過時,再根據用戶信息跳轉到不一樣的登陸頁

4.api管理

若是後端也是一套代碼。那api也能夠這樣進行管理,但若是沒有一個統一的前綴。能夠在axios設置一個統一的前綴例如proxy,這樣就解決了跨域的問題。

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子串去掉
        }
      }
    }
  },
複製代碼
相關文章
相關標籤/搜索