# 权限管理模块

# RBAC的权限设计思想

采用方案: RBAC的权限模型,RBAC(Role-Based Access control) ,也就是基于角色的权限分配解决方案

其权限模式如下:

image-20210217194732641

三个关键点: 员工用户, 角色, 权限

  1. 给员工分配角色
  2. 给角色分配权限

以你自己为例, 你进入一家公司, 入职, 人事将你录入系统 => 分配你的角色 (学生)

同角色有着相同的权限, 操作角色权限的同时, 所有该角色的用户对应权限, 就会同步更新

# 给员工分配角色

**目标**在员工管理页面,分配角色

image-20210217194808393

总结:给员工分配角色(1、只能分配一个角色;2、可以分配多个角色(1:n))

一旦把角色分配给用户,那么该用户登录系统后就可以访问对应的权限了。

# 新建分配角色弹框

image-20210131125420085

首先,新建分配角色窗体 employees/components/assign-role.vue

 <template>
  <el-dialog title="分配角色" :visible="showRoleDialog" @close="handleClose">
    <!-- 角色列表 -->
    <div>角色列表</div>
    <template #footer>
      <el-button type="primary" size="small">确定</el-button>
      <el-button size="small" @click='handleClose'>取消</el-button>
    </template>
  </el-dialog>
</template>
<script>
export default {
  name: 'AssignRole',
  props: {
    showRoleDialog: {
      type: Boolean,
      required: true
    }
  },
  methods: {
    handleClose () {
      this.$emit('update:showRoleDialog', false)
    }
  }
}
</script>

# 弹层的显示和关闭

  • 点击角色按钮显示弹层

image-20210131122251352

  • 注册组件
import AssignRole from './components/assign-role'

components: {
  AddEmployee,
  AssignRole
},
  
<assign-role :show-role-dialog.sync="showRoleDialog" :user-id="userId" />
  • 点击角色按钮, 记录id, 显示弹层
<el-button type="text" size="small" @click="showRoleBox(row.id)">角色</el-button>

showRoleBox(id) {
  this.userId = id
  this.showRoleDialog = true
}
  • 弹层的关闭
<template>
  <el-dialog class="assign-role" title="分配角色" :visible="showRoleDialog" @close="handleClose">
    <!-- el-checkbox-group选中的是 当前用户所拥有的角色  需要绑定 当前用户拥有的角色-->
    <el-checkbox-group>
      <!-- 选项 -->
    </el-checkbox-group>

    <template #footer>
      <div style="text-align: right">
        <el-button type="primary">确定</el-button>
        <el-button @click="handleClose">取消</el-button>
      </div>
    </template>
  </el-dialog>
</template>

<script>
export default {
  props: {
    showRoleDialog: {
      type: Boolean,
      default: false
    },
    // 用户的id 用来查询当前用户的角色信息
    userId: {
      type: String,
      default: null
    }
  },
  methods: {
    handleClose () {
      this.$emit('update:showRoleDialog', false)
    }
  }
}
</script>

总结:控制弹窗的显示和隐藏

  1. el-dialog组件的基本用法
  2. 父子之间传值的简化写法 sync 修饰符

# 获取角色列表

  • 基本布局
<el-dialog title="分配角色" :visible="showRoleDialog" @open="loadRoleList" @close="handleClose">
<el-checkbox-group v-model="roleIds">
  <el-checkbox label="110">管理员</el-checkbox>
  <el-checkbox label="113">开发者</el-checkbox>
  <el-checkbox label="115">人事</el-checkbox>
</el-checkbox-group>
  • 发送请求获取角色列表
import { reqGetRoles } from '@/api/setting'
export default {
  props: {
    showRoleDialog: {
      type: Boolean,
      default: false
    },
    // 用户的id 用来查询当前用户的角色信息
    userId: {
      type: String,
      default: null
    }
  },
  data() {
    return {
      list: [],
      roleIds: []
    }
  },
  methods: {
    // 获取所有的角色列表数据
    async loadRoleList () {
      const ret = await reqGetRoleList({
        page: 1,
        pagesize: 1000
      })
      this.allList = ret.data.rows
    },
    // 关闭弹窗
    handleClose () {
      this.$emit('update:showRoleDialog', false)
    }
  }
}
  • 渲染数据
<el-checkbox-group v-model="roleIds">
  <el-checkbox v-for="item in allList" :key="item.id" :label="item.id">
    {{ item.name }}
  </el-checkbox>
</el-checkbox-group>
  • 微调样式
<style lang="scss" scoped>
.assign-role {
  ::v-deep {
    .el-checkbox {
      font-size: 30px;
    }
  }
}
</style>

总结:

  1. 调用接口获取所有的角色列表数据
  2. 动态渲染CheckBox角色列表

# 获取用户的当前角色

  • 获取用户的当前角色, 进行回显
import { reqGetUserDetailById } from '@/api/user'

// 获取所有的角色列表数据
async loadRoleList () {
  // 获取所有角色数据
  const ret = await reqGetRoleList({
    page: 1,
    pagesize: 1000
  })
  this.allList = ret.data.rows
  // 获取当前用户数据
  const info = await reqGetUserDetailById(this.userId)
  this.roleList = info.data.roleIds
},
  • 基于open事件触发接口调用
<el-dialog class="assign-role" title="分配角色" :visible="showRoleDialog" @open="dialogOpen" @close="closeDialog">
methods: {
  loadRoleList() {
    this.loadRoleList()
  },
}

# 给员工分配角色

  • 分配角色接口 api/employees.js
export function reqAssignRoles(data) {
  return request({
    url: '/sys/user/assignRoles',
    data,
    method: 'put'
  })
}
  • 确定保存 assign-role
<el-button type="primary" @click="handleSubmit">确定</el-button>
async handleSubmit () {
  const ret = await reqAssignRoles({
    // 用户id
    id: this.userId,
    // 选中的角色列表
    roleIds: this.roleList
  })
  if (ret.success) {
    // 分配角色成功
    this.$message.success(ret.message)
    // 关闭弹窗
    this.handleClose()
  }
},

总结:给用户分配角色(一个用户可以分配多个角色)

# 添加loading效果

需求:显示角色列表的过程提供一个

<el-checkbox-group v-model="roleIds" v-loading="loading">
// 获取所有的角色列表数据
async loadRoleList () {
  // 开启加载状态
  this.loading = true
  // 获取所有角色数据
  const ret = reqGetRoleList({
    page: 1,
    pagesize: 1000
  })
  // this.allList = ret.data.rows
  // 获取当前用户数据
  const info = reqGetUserDetailById(this.userId)
  // this.roleList = info.data.roleIds
  // Promise.all方法的作用:并行触发多个异步任务,并且所有任务的结果都返回后触发then
  Promise.all([ret, info]).then(result => {
    // result是两个任务的结果
    this.allList = result[0].data.rows
    this.roleList = result[1].data.roleIds
    // 隐藏加载状态
    this.loading = false
  }).catch((err) => {
    console.log(err)
    this.$message.error('获取角色数据失败')
  })
},

总结: 共同点(都是并发触发多个任务);不同点:all保证所有任务都完成后获取异步结果;race只要有一个任务返回就得到该任务的结果,其他任务的结果不做处理。

  • Promise.all
  • Promise.race

# 回顾

  • 员工详情
    • 完成上传的流程
    • 上传的进度监控(基于ElementUI进度条;进度事件由腾讯云API提供)
    • 控制上传图片的数量
    • 完成头像的上传流程
    • 定制员工列表的头像展示(作用域插槽)
    • 显示图片的二维码(前端生成二维码:canvas)
    • 实现用户详细信息的打印操作
  • 权限管理
    • 熟悉RBAC这种权限验证的核心思想
    • 控制角色的添加操作
    • 显示要分配的角色列表
    • 显示默认的角色列表
    • 提交选中的角色给用户分配角色
    • 控制加载状态(性能优化)

# 权限点管理页面开发

image-20210418084456453

目标: 完成权限点页面的开发和管理 => 为了后面做准备

  1. 便于分配权限, 只有有权限了才能分配
  2. 只有分配好了权限, 有对应的权限规则, 才能控制路由(模块访问权), 才能控制按钮的显示(操作权)

# 新建权限点管理页面

  • 完成权限页面结构 src/views/permission/index.vue
<template>
  <div class="permission-container">
    <div class="app-container">
      <!-- 表格 -->
      <el-card>
        <div style="text-align: right; margin-bottom: 20px">
          <el-button type="primary" size="small">添加权限</el-button>
        </div>
        <el-table border>
          <el-table-column label="名称" />
          <el-table-column label="标识" />
          <el-table-column label="描述" />
          <el-table-column label="操作">
            <template>
              <el-button size="small" type="text">添加权限点</el-button>
              <el-button size="small" type="text">查看</el-button>
              <el-button size="small" type="text">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Permission'
}
</script>
  • 封装权限管理的增删改查请求 src/api/permisson.js
import request from '@/utils/request'

// 获取权限
export function reqGetPermissionList() {
  return request({
    method: 'get',
    url: '/sys/permission'
  })
}
// 新增权限
export function reqAddPermission(data) {
  return request({
    method: 'post',
    url: '/sys/permission',
    data
  })
}

// 更新权限
export function reqUpdatePermission(data) {
  return request({
    method: 'put',
    url: `/sys/permission/${data.id}`,
    data
  })
}

// 删除权限
export function reqDelPermission(id) {
  return request({
    method: 'delete',
    url: `/sys/permission/${id}`
  })
}
// 获取权限详情
export function reqGetPermissionDetail(id) {
  return request({
    method: 'get',
    url: `/sys/permission/${id}`
  })
}

# 动态渲染权限列表

<template>
  <div class="permission-container">
    <div class="app-container">
      <!-- 表格 -->
      <el-card>
        <div style="text-align: right; margin-bottom: 20px">
          <el-button type="primary" size="small">添加权限</el-button>
        </div>
        <el-table border :data="list">
          <el-table-column label="名称" prop="name" />
          <el-table-column label="标识" prop="code" />
          <el-table-column label="描述" prop="description" />
          <el-table-column label="操作">
            <template>
              <el-button size="small" type="text">添加权限点</el-button>
              <el-button size="small" type="text">查看</el-button>
              <el-button size="small" type="text">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
    </div>
  </div>
</template>

<script>
import { reqGetPermissionList } from '@/api/permission'

export default {
  name: 'Permission',
  data() {
    return {
      list: []
    }
  },
  created() {
    this.loadPermissionList()
  },
  methods: {
    // 加载权限列表数据
    async loadPermissionList () {
      try {
        const ret = await reqGetPermissionList()
        if (ret.success) {
          this.list = ret.data
        } else {
          this.$message.error('获取权限列表失败')
        }
      }
    }
  }
}
</script>

注意:但是这里的数据, 拿到的是列表式的数据, 但是希望渲染的是树形结构的, 所以需要处理

调用接口;获取数据;填充表格

# 获取权限数据并转化树形

这里,我们通过树形操作方法,将列表转化成层级数据

import { reqGetPermissionList } from '@/api/permission'
import { translateListToTreeData } from '@/utils'

export default {
  name: 'Permission',
  data() {
    return {
      list: []
    }
  },
  created () {
    this.loadPermissionList()
  },
  methods: {
    // 加载权限列表数据
    async loadPermissionList () {
      try {
        const ret = await reqGetPermissionList()
        if (ret.success) {
          // 把列表数据转换为树形数据
          this.list = translateListToTreeData(ret.data, '0')
          console.log(this.list)
        } else {
          this.$message.error('获取权限列表失败')
        }
      } catch (e) {
        console.log(e)
        this.$message.error('获取权限列表失败!')
      }
    }
  }
}
  • 给 table 表格添加 row-key 属性(不要添加冒号),table的列表数据必须包含children属性
<el-table border :data="list" row-key="id">
  <el-table-column label="名称" prop="name" />
  <el-table-column label="标识" prop="code" />
  <el-table-column label="描述" prop="description" />
  <el-table-column label="操作">
    <template>
      <el-button size="small" type="text">添加权限点</el-button>
      <el-button size="small" type="text">查看</el-button>
      <el-button size="small" type="text">删除</el-button>
    </template>
  </el-table-column>
</el-table>
  • 需要注意的是,当 type为1 时为一级权限, type为2 时为二级权限, 没有三级权限
<template>
  <div class="permission-container">
    <div class="app-container">
      <!-- 表格 -->
      <el-card>
        <div style="text-align: right; margin-bottom: 20px">
          <el-button type="primary" size="small">添加权限</el-button>
        </div>
        <el-table border :data="list" row-key="id">
          <el-table-column label="名称" prop="name" />
          <el-table-column label="标识" prop="code" />
          <el-table-column label="描述" prop="description" />
          <el-table-column label="操作">
            <template #default="{ row }">
              <el-button v-if="row.type === 1" size="small" type="text">添加权限点</el-button>
              <el-button size="small" type="text">查看</el-button>
              <el-button size="small" type="text">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
      </el-card>
    </div>
  </div>
</template>

总结:

  1. 展示树形表格结构
  2. 区分一级和二级权限

# 准备新增的弹层

  • 弹层结构
<!-- 新增编辑的弹层 -->
<el-dialog :visible="showDialog" title="弹层标题" @close="showDialog = false">
  <!-- 表单内容 -->
  <el-form label-width="100px">
    <el-form-item label="权限名称">
      <el-input />
    </el-form-item>
    <el-form-item label="权限标识">
      <el-input />
    </el-form-item>
    <el-form-item label="权限描述">
      <el-input />
    </el-form-item>
    <el-form-item label="权限启用">
      switch
    </el-form-item>
  </el-form>

  <template #footer>
    <div style="text-align: right;">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary">确定</el-button>
    </div>
  </template>
</el-dialog>

# 绑定数据

  • 基于文档准备数据
data() {
  return {
    list: [],
    showDialog: false,
    formData: {
      enVisible: '0', // 开启
      name: '', // 名称
      code: '', // 权限标识
      description: '', // 描述
      type: '', // 类型
      pid: '' // 添加到哪个节点下
    },
    // 表单验证
      rules: {
        name: [
          { required: true, message: '权限点名称不能为空', trigger: ['blur', 'change'] }
        ],
        code: [
          { required: true, message: '权限点编码不能为空', trigger: ['blur', 'change'] }
        ]
      }
  }
},
  • 表单绑定
<!-- 新增编辑的弹层 -->
<el-dialog :visible="showDialog" title="弹层标题" @close="showDialog = false">
    <!-- 表单内容 -->
    <el-form ref="authForm" :model="formData" :rules="rules" label-width="100px">
        <el-form-item label="权限名称" prop='name'>
            <el-input v-model="formData.name"/>
        </el-form-item>
        <el-form-item label="权限标识" prop='code'>
            <el-input v-model="formData.code"/>
        </el-form-item>
        <el-form-item label="权限描述" prop='description'>
            <el-input v-model="formData.description"/>
        </el-form-item>
        <el-form-item label="是否展示" prop='enVisible'>
            switch
        </el-form-item>
    </el-form>

  <template #footer>
    <div style="text-align: right;">
      <el-button @click="showDialog = false">取消</el-button>
      <el-button type="primary">确定</el-button>
    </div>
  </template>
</el-dialog>

总结:

  1. 表单验证流程:
    1. el-form(ref/model/rules)
    2. el-form-item(prop)
    3. el-input(v-model)

# Switch组件用法

<el-switch
  v-model="formData.enVisible"
  active-value="0"
  inactive-value="1"
  active-text="启用"
  inactive-text="禁用"
  active-color="#13ce66"
  inactive-color="#eee">
</el-switch>

总结:定制选项值的格式

active-value="0" 表示选中的值 inactive-value="1" 表示禁用的值

# 新增功能

新增有两个新增:

  1. 点击上面的添加权限, 添加是一级, 是一级访问权
<el-button type="primary" size="small" @click="handleAdd(1, '0')">添加权限</el-button>
  1. 点击下面的添加权限点, 添加的是二级, 是二级操作权(这里row.id 作为将来的pid)
<el-button v-if="row.type === 1" size="small" type="text" @click="handleAdd(2, row.id)">添加权限点</el-button>

提供事件处理函数

    // 添加弹窗动作
    handleAdd (type, pid) {
      // 设置权限的级别
      this.formData.type = type
      // 设置权限的父节点
      this.formData.pid = pid
      // 显示弹窗
      this.showDialog = true
    },
    // 提交表单
    handleSubmit () {
      this.$refs.authForm.validate(async valid => {
        if (!valid) return
        const ret = await reqAddPermission(this.formData)
        if (ret.success) {
          // 关闭弹窗
          this.showDialog = false
          // 刷新列表
          this.loadPermissionList()
          // 清空表单
          this.$refs.authForm.resetFields()
          this.formData = this.$options.data().formData
        } else {
          this.$message.error(ret.message)
        }
      })
    },

总结:

  1. 区分一级和二级菜单的type和pid
  2. 调用接口实现添加功能

# 删除功能

需求:1、绑定事件;2、提示删除;3、调用接口删除;4、刷新列表

  1. 注册点击事件
<el-button size="small" type="text" @click="handleDelete(row.id)">删除</el-button>
  1. 点击时发送删除请求
// 删除权限点
handleDelete (id) {
  this.$confirm('确认要删除吗?', '温馨提示').then(async () => {
    const ret = await reqDelPermission(id)
    if (ret.success) {
      // 删除成功
      this.loadPermissionList()
      this.showDialog = false
    } else {
      this.$message.error(ret.message)
    }
  }).catch((e) => {
    if (e !== 'cancel') {
      // 出错了
      this.$message.error('删除失败')
    }
  })
},

总结:

  1. 删除需要添加确认
  2. 调用接口删除流程

# 查看修改功能

1 注册点击事件

<el-button size="small" type="text" @click="toEdit(row.id)">查看</el-button>

2 查看时回显

// 控制关闭弹窗
handleClose () {
  this.showDialog = false
  this.$refs.authForm.resetFields()
  this.formData = this.$options.data().formData
},
// 编辑第一步(回填表单)
async toEdit (id) {
  const ret = await reqGetPermissionDetail(id)
  this.formData = ret.data
  this.showDialog = true
},

3 三元表达式定制标题

<el-dialog :visible="showDialog" :title="formData.id?'编辑权限':'添加权限'" @close="handleClose">

4 提交修改, 通过判断 formData 中有没有 id (新增是没有 id 的)

// 提交表单
handleSubmit () {
    this.$refs.authForm.validate(async valid => {
        if (!valid) return
        if (this.formData.id) {
            // 编辑权限
            const ret = await reqUpdatePermission(this.formData)
            if (ret.success) {
                this.loadPermissionList()
                this.handleClose()
            } else {
                this.$message.error(ret.message)
            }
        } else {
            // 添加权限
            const ret = await reqAddPermission(this.formData)
            if (ret.success) {
                // 关闭弹窗
                // this.showDialog = false
                // 刷新列表
                this.loadPermissionList()
                // 清空表单
                // this.$refs.authForm.resetFields()
                // this.formData = this.$options.data().formData
                this.handleClose()
            } else {
                this.$message.error(ret.message)
            }
        }
    })
},

5 关闭时重置数据

// 控制关闭弹窗
handleClose () {
    this.showDialog = false
    this.$refs.authForm.resetFields()
    this.formData = this.$options.data().formData
},

总结:

  1. 重用提交表单的方法(根据id的存在与否区分添加的编辑操作)

用户 -> 角色 -> 权限

# 给角色分配权限

# 新建分配权限弹出层

  • 准备弹层
<!-- 分配权限的弹层 -->
<el-dialog title="分配权限" :visible="showAuthDialog" @close="handleClose">
  <div>权限列表</div>
  <template #footer>
    <div style="text-align: right;">
      <el-button @click="handleClose">取消</el-button>
      <el-button type="primary">确定</el-button>
    </div>
  </template>
</el-dialog>
  • 注册事件
<el-button size="small" type="success" @click="assignAuth(row.id)">分配权限</el-button>
  • 提供数据方法
showAuthDialog: false, // 控制弹层的显示隐藏
ruleId: '' // 记录正在操作的角色

// 关闭授权的弹窗
handleClose () {
  this.showAuthDialog = false
},
// 给指定角色进行授权
assignAuth (id) {
  this.ruleId = id
  this.showAuthDialog = true
},

总结:

  1. 点击【分配权限】按钮记录当前要分配角色的id
  2. 控制弹窗的显示和隐藏

# 获取权限数据

  • 这里要进行权限分配, 先要请求拿到权限数据
permissionData: [] // 存储权限数据
  • 弹层显示, 发送请求, 给弹层注册open事件
<el-tree
  :data="permissionData"
  show-checkbox
  node-key="id"
  :default-expand-all="true"
  :default-checked-keys="[5]"
  :props="defaultProps">
</el-tree>
  • 拿到数据处理成功树形结构
// \获取权限列表数据
async loadAuthList () {
  try {
    const ret = await reqGetPermissionList()
    this.permissionData = translateListToTreeData(ret.data, '0')
  } catch {
    this.$message.error('获取权限列表失败')
  }
},

总结:

  1. 打开弹窗时,加载权限列表数据并转换为树形结构
  2. 树形组件的基本用法

# 结合树形控件显示

  • 基本展示
<el-tree
  :data="permissionData"
  :props="{ label: 'name' }"
  :default-expand-all="true"
  :show-checkbox="true"
  :check-strictly="true"
/>
  • show-checkbox 显示选择框

  • default-expand-all 默认展开

  • check-strictly 设置true, 可以关闭父子关联

# 回显默认的权限

树形结构认知: 回显数据, 需要有一些树形结构的认知

  1. node-key 唯一标识
  2. this.$refs.tree.setCheckedKeys([ ]) => 传入选中的node-key数组
<el-tree
  ref="tree"
  v-loading="treeLoading"
  :data="permissionData"
  :props="{ label: 'name' }"
  :default-expand-all="true"
  :default-checked-keys="defaultAuthList"
  :show-checkbox="true"
  :check-strictly="true"
  node-key="id"
/>

defaultAuthList: [] // 存储已选中的权限id列表
  • 发送请求, 获取已选中的权限 id 列表, 进行回显
// 获取权限列表数据
loadAuthList () {
  // 加载所有的权限
  const ret = reqGetPermissionList()
  // this.permissionData = translateListToTreeData(ret.data, '0')
  // 加载角色本来拥有的权限列表
  const auths = reqGetRoleDetail(this.ruleId)
  // this.defaultAuthList = auths.data.permIds
  // this.$nextTick(() => {
  //   this.$refs.authTree.setCheckedKeys(auths.data.permIds)
  // })
  Promise.all([ret, auths]).then(results => {
    this.permissionData = translateListToTreeData(results[0].data, '0')
    this.$nextTick(() => {
      this.$refs.authTree.setCheckedKeys(results[1].data.permIds)
    })
  }).catch(() => {
    this.$message.error('获取权限列表失败')
  })
},

总结:

  1. 可以基于属性default-checked-keys设置树节点的选中
  2. 也可以基于setCheckedKeys实例方法设置树节点的选中
  3. 如何获取选中的节点?getCheckedKeys()

# 给角色分配权限

  • 封装分配权限的api src/api/setting.js
// 给角色分配权限
export function reqAssignPerm(data) {
  return request({
    url: '/sys/role/assignPrem',
    method: 'put',
    data
  })
}
  • 分配权限
// 给角色分配权限的提交动作 
async handleAuthSubmit () {
  const ret = await reqAssignPerm({
    // 当前的角色id
    id: this.ruleId,
    // 选中权限点的节点(id)
    permIds: this.$refs.authTree.getCheckedKeys()
  })
  if (ret.success) {
    // 分配角色的权限成功
    this.loadRoleList()
    this.showAuthDialog = false
  } else {
    this.$message.error(ret.message)
  }
},

总结:重新选择权限,然后分配给角色

获取树的选中的节点方式:this.$refs.authTree.getCheckedKeys()

  • 总结
    • 权限点的管理
      • 权限列表的动态渲染
      • 添加权限
      • 熟悉Switch组件的用法
      • 删除权限点
      • 编辑权限点
    • 给角色分配权限(给用户分配角色类似)
      • 展示所有的权限列表(树形方式展示)
      • 展示并选中角色原有的权限
      • 重新选择权限并提交表单
      • 如何选中树形的组件节点?(用属性设置值;或者用户方法设置值)
      • 如何获取树形的组件的节点id?用组件的实例方法

不同的用户进入系统后,可以操作不同的功能(权限)

  • 用户
    • 给用户分配角色(支持多个角色的分配)
  • 角色
    • 给角色分配权限(一个角色可以拥有多个权限)
  • 权限

# 前端权限-页面访问权(路由)

# 权限受控的思路

到了最关键的环节,我们设置的权限如何应用?

在上面的几个小节中,我们已经把给用户分配了角色, 给角色分配了权限,

那么在用户登录获取资料的时候,会自动查出该用户拥有哪些权限,这个权限需要和我们的菜单含有路由有效结合起来

image-20210419084113013

  • menus表示左侧路由菜单的权限(一级权限)
  • points指的是路由组件中按钮的操作权限(二级权限)

而动态路由表其实就是根据用户的实际权限来访问的,接下来我们操作一下

image-20210218171005124

在权限管理页面中,我们设置了一个标识, 这个标识可以和我们的路由模块进行关联,

如果用户拥有这个标识,那么用户就可以 拥有这个路由模块,如果没有这个标识,就不能访问路由模块

# addRoutes 的基本使用

image-20210218171039521

  • router/index.js` 去掉 asyncRoutes 的默认合并,
const createRouter = () => new Router({
  // mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: [
    ...constantRoutes // 静态路由, 首页
    // ...asyncRoutes // 所有的动态路由
  ]
})
  • permission.js 我们通过 addRoutes 动态添加试一下
import { asyncRoutes } from '@/router/index'

router.beforeEach(async(to, from, next) => {
  ...
  	...
      if (!store.getters.userId) {
        // 调用获取信息的action
        const res = await store.dispatch('user/getUserInfo')
        console.log('进行权限处理', res)
        // 拿到权限信息之后, 应该根据权限信息, 从动态路由模块中筛选出, 需要追加的路由,
        // 追加到routes规则中, addRoutes
        router.addRoutes(asyncRoutes)
        next({
          ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
          replace: true // 重进一次, 不保留重复历史
        })
        return
      }
  	...
  ...
})

可以新增出来, 但是菜单却没有动态渲染出来 => router.options.routes (拿的是默认配置的项, 拿不到动态新增的) 不是响应式的!

为了能正确的显示菜单, 为了能够将来正确的获取到, 目前用户的路由, 我们需要用vuex管理routes路由数组

# 新建Vuex权限模块

可以在vuex中新增一个permission模块, 专门维护管理, 所有的路由 routes 数组 (响应式的)

在任何的组件中, 都可以非常方便的将来拿到 vuex 中存的 routes, 而不是去找 router.options.routes (拿的是默认配置)

src/store/modules/permission.js

import { constantRoutes } from '@/router'

const state = {
  // 路由表, 标记当前用户所拥有的所有路由
  routes: constantRoutes // 默认静态路由表
}
const mutations = {
  // otherRoutes登录成功后, 需要添加的新路由
  setRoutes(state, otherRoutes) {
    // 静态路由基础上, 累加其他权限路由
    state.routes = [...constantRoutes, ...otherRoutes]
  }
}
const actions = {}
export default {
  namespaced: true,
  state,
  mutations,
  actions
}

  • 在Vuex管理模块中引入permisson模块
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
import permission from './modules/permission'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    app,
    settings,
    user,
    permission
  },
  getters
})

export default store

总结:

  1. 把静态路由存储在Store的permission模块中
  2. 通过mutation把动态路由添加进去并且与静态路由进行合并

思考:我们为何要这样做呢?因为左侧菜单是通过this.$options.routes方式获取路由映射信息的,但是这种方式无法获取动态添加的路由映射信息,所以需要全局共享路由映射。

# Vuex-action筛选权限路由

将来登录成功时, 个人信息中会有 roles 的 menus 信息, 要基于menus来过滤出我们需要给用户 add 的路由,

但是**menus**中的标识又该怎么和路由对应呢?

可以将路由模块**name**属性命名和权限标识一致,这样只要标识能对上,就说明用户拥有了该权限

这一步,在我们命名路由的时候已经操作过了

接下来, vuex的 permission 中提供一个action,进行关联

import { asyncRoutes, constantRoutes } from '@/router'

const actions = {
  // 筛选路由权限
  filterRoutes(context, menus) {
    const otherRoutes = asyncRoutes.filter(item => {
      // 如果路由模块的首页name, 在menus数组中包含, 就是这个模块开放
      if (menus.includes(item.children[0].name)) {
        return true
      } else {
        return false
      }
    })
    context.commit('setRoutes', otherRoutes)
    return otherRoutes
  }
}

# 调用Action管理路由渲染菜单

  • 在 permission 拦截的位置,调用关联action, 获取新增routes,并且addRoutes
...
if (!store.state.user.userInfo.userId) {
  // 调用获取信息的action
  const { roles } = await store.dispatch('user/getUserInfo')
  // 调用action同步到vuex中
  const otherRoutes = await store.dispatch('permission/filterRoutes', roles.menus)
  // 动态新增路由
  router.addRoutes(otherRoutes)
  next({
    ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
    replace: true // 重进一次, 不保留重复历史
  })
  return
}
...
  • 在**src/store/getters.js**配置导出routes
const getters = {
  ...
  routes: state => state.permission.routes // 导出当前的路由
}
export default getters
  • 在左侧菜单 layout/components/Sidebar/index.vue 组件中, 引入routes, 使用vuex的routes动态渲染
computed: {
  ...mapGetters([
    'sidebar',
    'routes'
  ])
}

总结:

  1. 导航守卫触发action过滤用户动态路由权限,添加的store中
  2. 左侧菜单组件中取出store中的所有路由映射进行渲染。

注意:导航守卫中,必须显示查询用户信息,否则条件判断userId 会有问题(导致一直递归触发路由的导航守卫)

必须保证 if (!store.getters.userId) 条件仅仅成立一次( await store.dispatch('user/getUserInfo'))


  1. 添加员工
  2. 添加角色
  3. 添加权限点
  4. 给角色分配权限点
  5. 给员工分配角色
  6. 使用新员工账号登录系统
  7. 那么主页中显示的左侧路由菜单就是授权的权限

  1. 用户登录主页
  2. 登录时首先进入导航守卫
  3. 导航守卫中判断Store中的userId是否存在
  4. 如果第一次判断不存在,就需要手动查询用户信息(包含权限),然后根据权限过滤动态路由,把动态路由添加到Store中,那么下一次userId就存在了
  5. 进入主页之后,左侧菜单从Store中获取静态和动态路由合并的数据进行展示

# 处理刷新 404 的问题

页面刷新的时候,本来应该拥有权限的页面出现了404,这是因为404的匹配权限放在了静态路由中 (静态路由的404要删除)

我们需要将404放置到动态路由的最后

if (!store.state.user.userInfo.userId) {
  // 调用获取信息的action
  const { roles } = await store.dispatch('user/getUserInfo')
  // 调用action同步到vuex中
  const otherRoutes = await store.dispatch('permission/filterRoutes', roles.menus)
  // 动态新增路由
  router.addRoutes([...otherRoutes, { path: '*', redirect: '/404', hidden: true }])
  next({
    ...to, // next({ ...to })的目的,是保证路由添加完了再进入页面 (可以理解为重进一次)
    replace: true // 重进一次, 不保留重复历史
  })
  return
}

注意:这个位置添加的404路由映射,Store中看不到(没有添加到Store中),本来也不需要再Store中添加404路由映射。

# 退出时重置路由

退出时, 需要将路由权限重置 (恢复默认), 将来登录后, 再次追加

我们的**router/index.js**文件,发现一个重置路由方法

// 重置路由
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // 重新设置路由的可匹配路径
}

这个方法就是将路由重新实例化,相当于换了一个新的路由,之前**加的路由**就不存在了,需要在登出的时候, 调用一下即可

store/modules/user.js

import { resetRouter } from '@/router'

// 退出的action操作
logout(context) {
  // 1. 移除vuex个人信息
  context.commit('removeUserInfo')
  // 2. 移除token信息
  context.commit('removeToken')
  // 3. 重置路由
  resetRouter()
  // 4. 重置 vuex 中的路由信息
  context.commit('permission/setRoutes', [], { root: true })
},

注意: 这里后台的权限标识, 和 name 要一一对应, 例如: 叫 permissions, 就要把 name 改一下

export default {
  path: '/permission',
  component: Layout,
  children: [
    // 默认权限管理的首页
    {
      path: '',
      name: 'permissions', // name 加上, 后面有用
      component: () => import('@/views/permission/index'),
      meta: { title: '权限管理', icon: 'lock' }
    }
  ]
}

总结:

  1. 在store模块内部默认访问的mutation,在当前模块查找,如果希望在全局模块查找,需要设置root属性为true
  2. 如何在模块内部访问全局状态?通过action的context.rootState获取,getters中也可以获取全局状态。

# 前端权限-按钮操作权

# 按钮操作权的受控思路

当我们拥有了一个模块的访问权限之后,页面中的某些功能,用户可能有,也可能没有

这就是上小节,查询出来的数据中的**points**

当然现在是空, 所以首先需要在员工管理的权限点下, 新增一个删除权限点,并启用

我们要做的就是看看用户,是否拥有point-user-delete这个point,有就可以让删除能用,没有就隐藏或者禁用

# 演示功能-将方法挂载到原型

  • 配置getters
const getters = {
  ...,
  roles: state => state.user.userInfo.roles,
}
export default getters
  • 所有的页面中, 都要用到这个校验方法
// 扩展一个方法:验证按钮的权限
// code = POINT-EMPLOYEE-ADD
Vue.prototype.$isok = function (code) {
  // 获取所有的当前用户的权限点列表
  const points = store.getters.roles.points
  // 判断code是否包含在points中
  return points.includes(code)
}
  • 按钮控制
<template #right>
  <el-button v-if="$isok('POINT-EMPLOYEE-IMPORT')" type="warning" size="small" @click="$router.push('/import')">Excel导入</el-button>
  <el-button v-if="$isok('POINT-EMPLOYEE-EXPORT')" type="danger" size="small" @click="handleExport">Excel导出</el-button>
  <el-button v-if="$isok('POINT-EMPLOYEE-ADD')" type="primary" size="small" @click="handleAdd">新增员工</el-button>
</template>

总结:

  1. 根据登录成功后获取到的用户信息中的points数据判断相关的按钮是否具有操作权限

# 使用 Mixins 混入

有一些通用的逻辑,其他组件可以共享(不需要每一个组件都实现这个功能)

  • mixins/hello.js`
// 这是一个打招呼的mixin, 只要混入了这个mixin, 就可以created打招呼
export default {
  // vue的配置项, data, methods, created, ....
  created() {
    console.log('created: hello, 你好哇')
    this.sayHi()
  },
  methods: {
    sayHi() {
      console.log('调用方法, sayHi: hello, 你好哇')
    }
  }
}
  • 局部混入
import { mapGetters } from 'vuex'
import hello from '@/mixins/hello'

export default {
  name: 'Dashboard',
  mixins: [hello],
  computed: {
    ...mapGetters([
      'name'
    ])
  }
}
  • 全局混入 (注意点: 一旦配置, 所有的组件, 全都会被混入 (慎用))
import hello from '@/mixins/hello'
Vue.mixin(hello)

总结

  1. 局部混入的方式仅仅影响混入的相关组件
  2. 全局混入会影响所有的组件

# 计算属性的用法补充

  • 计算属性支持set方式修改值
// }
fullName: {
  get: function () {
    return this.firstName + ' ' + this.lastName
  },
  set: function (value) {
    const arr = value && value.split(' ')
    if (arr.length === 2) {
      this.firstName = arr[0]
      this.lastName = arr[1]
    }
  }
}