vue后台_登录权限
登录权限控制包含着这么几个方面的含义:
1)不同的权限对应不同的路由
2)侧边栏需要根据不同的权限,异步生成
登录:使用用户名和密码,登录成功后返回用户的token(防止XSS攻击),将此token存放在cookie中(保证刷新页面后依旧能够记住用户的登录状态)
之后,前端根据token去拉取一个user_info的接口,获取用户详细信息,包括role
根据用户的role,动态计算对应权限的路由,使用router.addRoutes动态挂载这些路由
涉及:vue-router,vuex,axios拦截等等
因为vue-router必须在vue实例化之前就挂载,所以不太方便动态改变,不过好在vue-router2.2之后新增了router.addRouters
基本思路如下:
- 创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页面。
- 当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路由表。
- 调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。
- 使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。
1)vue-router的构造配置,分为两部分,同步路由(包括首页、登录页、以及不实使用权限的公用页面),以及可能根据用户权限进行异步加载的路由
import Vue from 'vue' import Router from 'vue-router' const login = (resolve) => require(['../components/pages/login/login.vue'], resolve); const home = (resolve) => require(['../components/common/home/home.vue'], resolve); const basicInfo = (resolve) => require(['../components/pages/basicInfo/basicInfo.vue'], resolve); const productDayForm = (resolve) => require(['../components/pages/productDayForm/productDayForm.vue'], resolve); const provinceDayForm = (resolve) => require(['../components/pages/provinceDayForm/provinceDayForm.vue'], resolve); const productMonthForm = (resolve) => require(['../components/pages/productMonthForm/productMonthForm.vue'], resolve); const provinceMonthForm = (resolve) => require(['../components/pages/provinceMonthForm/provinceMonthForm.vue'], resolve); const personCenter = (resolve) => require(['../components/pages/personCenter/personCenter.vue'], resolve); const userManage = (resolve) => require(['../components/pages/userManage/userManage.vue'], resolve); // 测试用 const dashboard = (resolve) => require(['../components/pages/dashboard/dashboard.vue'], resolve); const errorPage = (resolve) => require(['../components/pages/404/404.vue'], resolve); const passwordReset = (resolve) => require(['../components/pages/passwordReset/passwordReset.vue'], resolve); const messageTemplate = (resolve) => require(['../components/pages/messageTemplate/messageTemplate.vue'], resolve); const messageEdit = (resolve) => require(['../components/pages/messageEdit/messageEdit.vue'], resolve); const messageDetail = (resolve) => require(['../components/pages/messageDetail/messageDetail.vue'], resolve); const IntelHold = (resolve) => require(['../components/pages/IntelHold/IntelHold.vue'], resolve); const Test = () => import('../components/pages/Test/Test.vue'); Vue.use(Router) export const constantRouterMap = [{ path: '/login', component: login, name: 'login', meta: { hidden: true } }, { path: '/resetPassword', component: passwordReset, name: 'resetPassword', meta: { hidden: true } } ]; export const asyncRouterMap = [{ path: '/', component: home, name: 'root', redirect: '/static', meta: { dropdown: false }, children: [{ path: 'static', component: dashboard, name: 'dashboard', meta: { title: '数据统计', icon: 'icon-attendance' } }] }, { path: '/dayForm', component: home, redirect: '/dayForm/productDayForm', name: 'dayForm', meta: { title: '日报表', icon: 'icon-calendar', dropdown: true }, children: [{ path: 'productDayForm', component: productDayForm, name: 'productDayForm', meta: { title: '产品日报表' } }, { path: 'provinceDayForm', component: provinceDayForm, name: 'provinceDayForm', meta: { title: '区域日报表' } }] }, { path: '/monthForm', component: home, redirect: '/monthForm/productMonthForm', name: 'monthForm', meta: { title: '月报表', icon: 'icon-cangneishicao', dropdown: true }, children: [{ path: 'productMonthForm', component: productMonthForm, name: 'productMonthForm', meta: { title: '产品月报表' } }, { path: 'provinceMonthForm', component: provinceMonthForm, name: 'provinceMonthForm', meta: { title: '区域月报表' } }] }, { path: '/detail', component: home, redirect: '/detail/basicInfo', name: 'detail', meta: { title: '明细', icon: 'icon-delivery', dropdown: true }, children: [{ path: 'basicInfo', component: basicInfo, name: 'basicInfo', meta: { title: '基本信息' } }] }, { path: '/message', component: home, redirect: '/message/messageTemplate', name: 'message', meta: { title: '短信维系', icon: 'icon-chat', dropdown: true }, children: [{ path: 'messageTemp', component: messageTemplate, name: 'messageTemplate', meta: { title: '经典模型' }, children: [{ path: ':id', component: messageDetail, name: 'messageDetail', meta: { hidden: true } }] }, { path: 'messageEdit', component: messageEdit, name: 'messageEdit', meta: { title: '快速建模' } }] }, { path: '/', component: home, name: 'root', redirect: '/static', meta: { icon: 'el-icon-star-on', dropdown: false }, children: [{ path: 'intelHold', component: IntelHold, name: 'IntelHold', meta: { title: '智慧维系', icon: 'icon-bank-card' } }, { path: 'userManage', component: userManage, name: 'userManage', meta: { role: ['admin'], title: '用户管理', icon: 'icon-power' } }, { path: 'personCenter', component: personCenter, name: 'personCenter', meta: { title: '个人中心', icon: 'icon-user' } }, { path: 'test', component: Test, name: 'Test', meta: { title: '测试', icon: 'icon-user' } }] }, { path: '*', component: errorPage, name: '404', meta: { hidden: true } }]; export default new Router({ scrollBehavior: () => ({ y: 0 }), routes: constantRouterMap });
2)用户信息:包括role,name,token,avatar,token等都使用vuex来集中管理
import { loginByEmail, logout, getInfo } from 'api/login'; import { getToken, setToken, removeToken } from 'utils/auth'; const user = { state: { user: '', status: '', code: '', token: getToken(), name: '', avatar: '', introduction: '', roles: [], setting: { articlePlatform: [] } }, mutations: { SET_CODE: (state, code) => { state.code = code; }, SET_TOKEN: (state, token) => { state.token = token; }, SET_INTRODUCTION: (state, introduction) => { state.introduction = introduction; }, SET_SETTING: (state, setting) => { state.setting = setting; }, SET_STATUS: (state, status) => { state.status = status; }, SET_NAME: (state, name) => { state.name = name; }, SET_AVATAR: (state, avatar) => { state.avatar = avatar; }, SET_ROLES: (state, roles) => { state.roles = roles; }, LOGIN_SUCCESS: () => { console.log('login success') }, LOGOUT_USER: state => { state.user = ''; } }, actions: { // 邮箱登录 LoginByEmail({ commit }, userInfo) { const email = userInfo.email.trim(); return new Promise((resolve, reject) => { loginByEmail(email, userInfo.password).then(response => { console.log('login response', response); // debugger const data = response.data; setToken(response.data.token); commit('SET_TOKEN', data.token); resolve(); }).catch(error => { reject(error); }); }); }, // 获取用户信息 GetInfo({ commit, state }) { return new Promise((resolve, reject) => { getInfo(state.token).then(response => { const data = response.data; commit('SET_ROLES', data.role); commit('SET_NAME', data.name); commit('SET_AVATAR', data.avatar); commit('SET_INTRODUCTION', data.introduction); resolve(response); }).catch(error => { reject(error); }); }); }, // 第三方验证登录 LoginByThirdparty({ commit, state }, code) { return new Promise((resolve, reject) => { commit('SET_CODE', code); loginByThirdparty(state.status, state.email, state.code).then(response => { commit('SET_TOKEN', response.data.token); setToken(response.data.token); resolve(); }).catch(error => { reject(error); }); }); }, // 登出 LogOut({ commit, state }) { return new Promise((resolve, reject) => { logout(state.token).then(() => { commit('SET_TOKEN', ''); commit('SET_ROLES', []); removeToken(); resolve(); }).catch(error => { reject(error); }); }); }, // 前端 登出 FedLogOut({ commit }) { return new Promise(resolve => { commit('SET_TOKEN', ''); removeToken(); resolve(); }); }, // 动态修改权限 ChangeRole({ commit }, role) { return new Promise(resolve => { commit('SET_ROLES', [role]); commit('SET_TOKEN', role); setToken(role); resolve(); }) } } }; export default user;
3) 不同用户角色的路由权限信息,也使用vuex进行集中管理
import { asyncRouterMap, constantRouterMap } from 'src/router' /** * 通过meta.role判断是否与当前用户权限匹配 * @param roles * @param route */ function hasPermission(roles, route) { if (route.meta && route.meta.role) { return roles.some(role => route.meta.role.indexOf(role) >= 0) } else { return true } } /** * 递归过滤异步路由表,返回符合用户角色权限的路由表 * @param asyncRouterMap * @param roles */ function filterAsyncRouter(asyncRouterMap, roles) { const accessedRouters = asyncRouterMap.filter(route => { if (hasPermission(roles, route)) { if (route.children && route.children.length) { route.children = filterAsyncRouter(route.children, roles) } return true } return false }) return accessedRouters } const permission = { state: { routers: constantRouterMap, addRouters: [] }, mutations: { SET_ROUTERS: (state, routers) => { state.addRouters = routers state.routers = constantRouterMap.concat(routers) } }, actions: { GenerateRoutes({ commit }, data) { return new Promise(resolve => { const { roles } = data let accessedRouters if (roles.indexOf('admin') >= 0) { accessedRouters = asyncRouterMap } else { accessedRouters = filterAsyncRouter(asyncRouterMap, roles) } commit('SET_ROUTERS', accessedRouters); resolve(); }) } } }; export default permission;
4) 使用router.beforeEach注册一个全局前置守卫
// permissiom judge function hasPermission(roles, permissionRoles) { if (roles.indexOf('admin') >= 0) return true; // admin权限 直接通过 if (!permissionRoles) return true; return roles.some(role => permissionRoles.indexOf(role) >= 0) } // register global progress. const whiteList = ['/login', '/authredirect', '/reset', '/sendpwd'];// 不重定向白名单 router.beforeEach((to, from, next) => { NProgress.start(); // 开启Progress if (getToken()) { // 判断是否有token if (to.path === '/login') { next({ path: '/' }); } else { if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetInfo').then(res => { // 拉取user_info const roles = res.data.role; store.dispatch('GenerateRoutes', { roles }).then(() => { // 生成可访问的路由表 router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 next({ ...to }); // hack方法 addRoutes之后next()可能会失效,因为可能next()的时候add没有完成,可以通过next({...to})此时之前的导航会被放弃,重新发起新的导航,来避免这个问题 }) }).catch(() => { store.dispatch('FedLogOut').then(() => { next({ path: '/login' }); }) }) } else { // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ if (hasPermission(store.getters.roles, to.meta.role)) { next();// } else { next({ path: '/401', query: { noGoBack: true } }); } // 可删 ↑ } } } else { if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next('/login'); // 否则全部重定向到登录页 NProgress.done(); // 在hash模式下 改变手动改变hash 重定向回来 不会触发afterEach 暂时hack方案 ps:history模式下无问题,可删除该行! } } }); router.afterEach(() => { NProgress.done(); // 结束Progress });
5)侧边栏渲染
侧边栏根据之前计算出的当前用户权限对应的路由(vuex管理),进行渲染,同时为了支持无限嵌套路由,使用了递归组件
const getters = { sidebar: state => state.app.sidebar, visitedViews: state => state.app.visitedViews, token: state => state.user.token, avatar: state => state.user.avatar, name: state => state.user.name, introduction: state => state.user.introduction, status: state => state.user.status, roles: state => state.user.roles, setting: state => state.user.setting, permission_routers: state => state.permission.routers, addRouters: state => state.permission.addRouters }; export default getters
<template> <el-menu mode="vertical" theme="dark" unique-opened :default-active="$route.path" :collapse="isCollapse"> <sidebar-item :routes='permission_routers'></sidebar-item> </el-menu> </template> <script> import { mapGetters } from 'vuex'; import SidebarItem from './SidebarItem'; export default { components: { SidebarItem }, computed: { ...mapGetters([ 'permission_routers', 'sidebar' ]), isCollapse() { return !this.sidebar.opened } } } </script>
<template> <div class='menu-wrapper'> <template v-for="item in routes"> <router-link v-if="!item.hidden&&item.noDropdown&&item.children.length>0" :to="item.path+'/'+item.children[0].path"> <el-menu-item :index="item.path+'/'+item.children[0].path" class='submenu-title-noDropdown'> <icon-svg v-if='item.icon' :icon-class="item.icon"></icon-svg><span slot="title">{{item.children[0].name}}</span> </el-menu-item> </router-link> <el-submenu :index="item.name" v-if="!item.noDropdown&&!item.hidden"> <template slot="title"> <icon-svg v-if='item.icon' :icon-class="item.icon"></icon-svg><span>{{item.name}}</span> </template> <template v-for="child in item.children" v-if='!child.hidden'> <sidebar-item class='nest-menu' v-if='child.children&&child.children.length>0' :routes='[child]'> </sidebar-item> <router-link v-else :to="item.path+'/'+child.path"> <el-menu-item :index="item.path+'/'+child.path"> <icon-svg v-if='child.icon' :icon-class="child.icon"></icon-svg><span>{{child.name}}</span> </el-menu-item> </router-link> </template> </el-submenu> </template> </div> </template> <script> export default { name: 'SidebarItem', props: { routes: { type: Array } } } </script> <style rel="stylesheet/scss" lang="scss" scoped> </style>
这里考虑一个问题:对于同时具有上侧和左侧导航的页面来说,当切换上侧导航时,左侧导航栏也对应不同(可参见element-ui的官方文档效果),这种是怎么实现的?
一种待验证的思路:在vuex中设置一个currentRoutes的state,在路由守卫(感觉选择路由独享的守卫,即在路由配置上直接定义beforeEnter守卫比全局守卫更合适)中切换currentRoutes的值,侧边栏根据currentRoutes的值进行渲染
6)axios封装
通过request拦截器在每个请求的头部塞入token,好让后台对请求进行权限验证;并在response拦截器中,判断服务端返回的特殊状态码,进行统一处理,如没有权限或者token失效(为了安全考虑,一般一个token的有效期都是session,就是当浏览器关闭就丢失了,重新打开浏览器需要重新验证,后台也会在比如每周一个固定时间点重新刷新token,强制所有用户重新登录一次)等
import axios from 'axios'; import { Message } from 'element-ui'; import store from '../store'; import { getToken } from 'utils/auth'; // 创建axios实例 const service = axios.create({ baseURL: process.env.BASE_API, // api的base_url timeout: 5000 // 请求超时时间 }); // request拦截器 service.interceptors.request.use(config => { // Do something before request is sent if (store.getters.token) { config.headers['X-Token'] = getToken(); // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改 } return config; }, error => { // Do something with request error console.log(error); // for debug Promise.reject(error); }) // respone拦截器 service.interceptors.response.use( response => { return response; }, /** * 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页 * 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中 */ // const res = response.data; // if (res.code !== 20000) { // Message({ // message: res.message, // type: 'error', // duration: 5 * 1000 // }); // // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了; // if (res.code === 50008 || res.code === 50012 || res.code === 50014) { // MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', { // confirmButtonText: '重新登录', // cancelButtonText: '取消', // type: 'warning' // }).then(() => { // store.dispatch('FedLogOut').then(() => { // location.reload();// 为了重新实例化vue-router对象 避免bug // }); // }) // } // return Promise.reject(error); // } else { // return response.data; // } error => { console.log('err' + error); // for debug Message({ message: error.message, type: 'error', duration: 5 * 1000 }); return Promise.reject(error); } ) export default service;