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子串去掉
        }
      }
    }
  },
复制代码
相关文章
相关标签/搜索