zoukankan      html  css  js  c++  java
  • 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封装
  • 相关阅读:
    SQL 2008 数据库只读 修改
    java List 简单使用
    鼠标右键菜单 删除
    SQL distinct
    日系插画学习笔记(五):日系角色脸部画法-1头部
    日系插画学习笔记(四):基础人体结构
    日系插画学习笔记(三):光影与结构
    日系插画学习笔记(二):结构与透视
    日系插画学习笔记(一):SAI软件基础
    spring mvc 静态资源版本控制
  • 原文地址:https://www.cnblogs.com/bobodeboke/p/9041201.html
Copyright © 2011-2022 走看看