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
});
router.js

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;
store/user.js

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;
store/permission.js

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
});
节选自main.js(路由守卫)

 

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
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>
sidebar.vue
<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>
sidebarItem.vue

这里考虑一个问题:对于同时具有上侧和左侧导航的页面来说,当切换上侧导航时,左侧导航栏也对应不同(可参见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;
axios封装

 

posted @ 2018-05-15 15:29  bobo的学习笔记  阅读(1189)  评论(0编辑  收藏  举报