一般后台系统都会有权限限制,权限控制需要前后端一起控制,前端的权限管理校验主要的目的是过滤不该有的请求和操作,减少服务端压力。
前端权限控制应该分为四个方面,接口权限、按钮权限,页面权限,路由权限。

接口权限

一般会进行接口返回参数校验,如果参数返回401 (根据后端定义的返回值),返回登录页重新登录。
登录完成拿到token,将token存在本地存储中,接口在请求时头部携带token(axios请求拦截器实现)。

  1. import axios, {AxiosRequestConfig} from "axios";
  2. import store from "@/config/store";
  3. import {Logout} from "@/pages/login/logout"
  4. // 环境变量
  5. const API_PATH = process.env.REACT_APP_API_PATH;
  6. axios.defaults.validateStatus = () => true;
  7. axios.defaults.timeout = 60000;
  8. axios.defaults.baseURL = API_PATH;
  9. // 请求前
  10. axios.interceptors.request.use((config) => {
  11. // 获取全局数据
  12. const globalState = store.getState().globalState;
  13. // 设置请求头
  14. config.headers = config.headers || {};
  15. // 设置参数添加token
  16. config.headers.Authorization = globalState.userData.token || ""
  17. return config;
  18. }, (error => {
  19. }))
  20. // 请求后
  21. axios.interceptors.response.use((config) => {
  22. return config
  23. }, error => {
  24. })
  25. //eslint-disable-next-line
  26. export default (config: AxiosRequestConfig) => new Promise((resolve) => {
  27. axios(config).then(res => {
  28. if (res?.data?.meta?.code === 401) {
  29. return Logout()
  30. } else {
  31. resolve(res?.data || {})
  32. }
  33. }).catch(() => ({}))
  34. });
  35. export const Logout = () => {
  36. window.location.href = "/login";
  37. }

按钮权限

一个页面会有新增,删除,编辑等等按钮。不同用户应该是有不同操作权限的。
我们不妨定义权限列表或者权限码

  1. //把所有的按钮级别的权限文本存放在这里,统一管理
  2. export const permissionsMaps = {
  3. add_shop: 'add_shop',
  4. add_account: 'add_account',
  5. del_account: 'del_account'
  6. }

react

  • 我们提前需要和后端沟通好,通过给用户分配权限(权限码),通过接口的形式,将后端返回的权限码或者按钮权限列表存起来。
  • 通过自定义组件和插槽

后端返回结果
权限管理 - 图1

  1. import React from 'react'
  2. /**
  3. * @description:
  4. * @param {*} children 子组件内容
  5. * @param {*} permission 权限类型
  6. * @param {*} type 无权限处理类型,disabled为禁用,visible为不显示,默认不显示
  7. * @return {*}
  8. */
  9. export default function AuthComponents ({ children, permission, type = 'visible', ...rest }) {
  10. // children为插槽的内容
  11. // rest接收外部组件传入的方法和其他属性
  12. const permissions = JSON.parse(localStorage.getItem('permissions'))
  13. // 判断显示类型
  14. if (type === 'visible') {
  15. // 复制出一个新节点,将组件库中某些给按钮添加的事件绑定到新节点上
  16. return permissions.includes(permission) ? React.cloneElement(children, rest) : null
  17. } else {
  18. // 复制出一个新节点,在新节点上添加禁用属性
  19. return React.cloneElement(children, {
  20. disabled: true,
  21. title: "对不起,您没有权限"
  22. })
  23. }
  24. }
  1. import { permissionsMaps } from '../../configs/constants'
  2. import AuthComponents from '../../components/AuthComponents'
  3. // Popconfirm 为antd中的组件,给Button传递了事件,所以在自定义组件中需要用rest参数接收,并传递给自定义组件
  4. <Popconfirm title="确定删除吗?" onConfirm={() => del(item._id)} >
  5. <AuthComponents permission={permissionsMaps.del_account} type={'disabled'}>
  6. <Button type='primary' size='small'>删除</Button>
  7. </AuthComponents>
  8. </Popconfirm>
  9. <AuthComponents permission={permissionsMaps.add_account} type={'visible'}>
  10. <Button>添加账号</Button>
  11. </AuthComponents>

vue

  • 我们提前和后端约定好按钮的名字,后端会返回一个按钮权限列表,通过vuex存储在全局store中。
  • 然后我们根据权限列表使用v-if指令或者 绑定disabled属性达到相应权限效果。
  • 当然更好的最好是自己写一个自定义权限指令,实质就是根据相应权限操作dom

比如概览页面的编辑按钮 名字先和后端定义好叫做overview-edit

  1. // overviwe.vue overview是概览页面的路由名
  2. ...
  3. <button v-auth='edit'>
  4. ...
  5. //util.js 全局注册自定义指令
  6. Vue.directive('auth', {
  7. inserted: function (el, binding, vnode) {
  8. const optName = binding.arg
  9. const authName = `${routeName}-${optName}`
  10. //这里根据路由名和操作类型拼出按钮名 overview-edit
  11. const btnAuthList = store.state.auth.btnAuthList
  12. if (btnAuthList[authName]===0) {
  13. // 按钮权限为0则移除dom
  14. el.parentNode.removeChild(el)
  15. } else if (btnAuthList[authName]===1) {
  16. // 按钮权限为1则禁用按钮
  17. vnode.componentInstance.disabled = true
  18. }
  19. }
  20. })
  21. // 登录的时候接受按钮权限并存在vuex里面
  22. const {btnAuthList} = login()
  23. vuex.state.btnAuthList = btnAuthList

菜单权限

获取菜单权限列表,动态递归生成菜单
这个菜单权限列表可以是后台直接返回你的,也可以是你注册路由的时候写在meta里面的菜单信息,后台返回路由权限,你根据meta信息动态算出的菜单权限。
至于菜单肯定是根据菜单权限递归生成的

react

  1. //模拟后端返回的菜单数据
  2. const items = [
  3. { label: '首页', key: '/nav/home' },
  4. { label: '订单管理', key: '/nav/orderManage' },
  5. {
  6. label: '店铺管理', key: 'shop-management', icon: <ShopOutlined />,
  7. children: [
  8. { label: '店铺列表', key: '/nav/shopList' },
  9. { label: '添加店铺', key: '/nav/shopAdd', roles: ['超级管理员'] }
  10. ]
  11. },
  12. {
  13. label: '账号管理', key: 'accounts-management', icon: <UserOutlined />,
  14. children: [
  15. { label: '账号列表', key: '/nav/accountList' },
  16. { label: '添加账号', key: '/nav/accountAdd', roles: ['超级管理员'] }
  17. ]
  18. },
  19. {
  20. label: '商品管理', key: 'goods-management', icon: <ShoppingOutlined />,
  21. children: [
  22. { label: '商品列表', key: '/nav/goodsList' },
  23. { label: '新增商品', key: '/nav/goodsAdd', roles: ['超级管理员'] }
  24. ]
  25. },
  26. {
  27. label: '销售统计', key: 'sales-statistics', icon: <BarChartOutlined />,
  28. children: [
  29. { label: '订单统计', key: '/nav/orderStatistics' },
  30. { label: '商品统计', key: '/nav/goodsStatistics' }
  31. ]
  32. },
  33. { label: '组件通信', key: '/nav/communication', roles: ['超级管理员'] },
  34. { label: '按钮权限', key: '/nav/auth' }
  35. ]

根据当前用户的权限等级筛选

  1. export default function filterAuth (array) {
  2. const userInfo = JSON.parse(localStorage.getItem('userInfo'))
  3. if (!userInfo) {
  4. return array
  5. }
  6. const { role } = userInfo
  7. return array.filter(item => !item.roles || item.roles.includes(role)).map(item => {
  8. // 如果有子菜单
  9. // 单独定义变量,避免修改原数组
  10. let o = {
  11. ...item,
  12. }
  13. if (item.children) {
  14. o.children = filterAuth(item.children)
  15. }
  16. return o
  17. })
  18. }

vue

  1. // 所有菜单
  2. menus: [
  3. {
  4. id: '1',
  5. name: '学生管理',
  6. icon: '',
  7. children: [
  8. { id: '1-1', name: '学生列表', icon: '', path: 'StudentsList' },
  9. { id: '1-2', name: '新增学生', icon: '', path: 'StudentsAdd' }
  10. ]
  11. },
  12. {
  13. id: '2',
  14. name: '班级管理',
  15. icon: '',
  16. children: [
  17. {
  18. id: '2-1',
  19. name: '班级列表',
  20. icon: '',
  21. path: 'ClassesList'
  22. },
  23. {
  24. id: '2-2',
  25. name: '新增班级',
  26. icon: '',
  27. path: 'ClassesAdd'
  28. }
  29. ]
  30. },
  31. {
  32. id: '3',
  33. name: '专业管理',
  34. icon: '',
  35. children: [
  36. {
  37. id: '3-1',
  38. name: '专业列表',
  39. icon: '',
  40. path: 'SubjectsList'
  41. }
  42. ]
  43. },
  44. {
  45. id: '4',
  46. name: '课程管理',
  47. icon: '',
  48. children: [
  49. {
  50. id: '4-1',
  51. name: '课程列表',
  52. icon: '',
  53. path: 'CoursesList'
  54. },
  55. {
  56. id: '4-2',
  57. name: '新增课程',
  58. icon: '',
  59. path: 'CoursesAdd'
  60. }
  61. ]
  62. },
  63. {
  64. id: '5',
  65. name: '教师管理',
  66. icon: '',
  67. children: [
  68. {
  69. id: '5-1',
  70. name: '教师列表',
  71. icon: '',
  72. path: 'teachersList'
  73. },
  74. {
  75. id: '5-2',
  76. name: '新增教师',
  77. icon: '',
  78. path: 'teachersAdd'
  79. }
  80. ]
  81. },
  82. {
  83. id: '6',
  84. name: '其他功能',
  85. icon: '',
  86. children: [
  87. {
  88. id: '6-1',
  89. name: '计数器',
  90. icon: '',
  91. path: 'Counter'
  92. }
  93. ]
  94. }
  95. ]
  96. // 假设当前用户权限下可访问的菜单
  97. authMenus: ['SubjectsList', 'ClassesList', 'ClassesAdd']
  1. // 用户身份权限菜单筛选
  2. filterMenus () {
  3. return this.menus.map(item => {
  4. const children = item.children.filter(child => {
  5. return this.authMenus.includes(child.path)
  6. })
  7. return {
  8. ...item,
  9. children
  10. }
  11. }).filter(item => {
  12. return item.children.length > 0
  13. })
  14. }
  15. },

刷新后界面的菜单消失问题:

  • 因为菜单数据是登录之后才获取到的,获取菜单数据之后,就存放在vuex中,vuex中的rightList的默认值就是空数组。一刷新界面,Vuex中的数据会重新初始化,所以会变成空的数组
  • 因此,需要将权限数据存储在sessionstorage中,并让其和vuex中的数据保持同步
  • 退出登录时,需要清除sessionStorage中的数据,并将vuex中的数据重置(调用location.reload()重新刷新页面就可以)

    路由权限

    上面的菜单权限虽然做到能看不见菜单,但是可以通过直接输入url的方式去没有权限的页面,这种情况需要路由权限来阻止。

    react

    和上面菜单权限一样,也是根据当前用户的权限筛选可以访问的路由
    react中没有路由守卫
    1. // 所有路由菜单
    2. const routes = [
    3. {
    4. path: "/",
    5. element: <Navigate to='/login' />
    6. },
    7. {
    8. path: "/login",
    9. element: <Login />
    10. },
    11. {
    12. path: "/nav",
    13. element: <Nav />,
    14. children: [
    15. { path: "home", index: true, element: <Home /> },
    16. { path: "orderManage", element: <OrderManage /> },
    17. { path: "shopList", element: <ShopList /> },
    18. { path: "shopAdd", element: <ShopAdd />, roles: ['超级管理员'] },
    19. { path: "accountList", element: <AccountList /> },
    20. { path: "accountAdd", element: <AccountAdd />, roles: ['超级管理员'] },
    21. { path: "goodsList", element: <GoodsList /> },
    22. { path: "goodsAdd", element: <GoodsAdd />, roles: ['超级管理员'] },
    23. { path: "orderStatistics", element: <OrderStatistics /> },
    24. { path: "goodsStatistics", element: <GoodsStatistics /> },
    25. { path: "profile", element: <Profile /> },
    26. { path: "communication", element: <Communication />, roles: ['超级管理员'] },
    27. { path: "auth", element: <Auth /> },
    28. ]
    29. },
    30. {
    31. path: "*",
    32. element: <NotFound />
    33. }
    34. ]
    35. // filterAuth权限筛选函数 见上菜单权限-react
    36. export default function MyRoutes () {
    37. const authRoutes = filterAuth(routes);
    38. const element = useRoutes(authRoutes);
    39. return element
    40. }

    vue

    使用动态路由 ```jsx import HomeView from ‘../views/home/HomeView.vue’ import StudentsList from ‘../views/students/StudentsList.vue’ import StudentsAdd from “../views/students/StudentsAdd.vue”; import StudentsUpdate from “../views/students/StudentsUpdate.vue”; import ChartsView from ‘../views/charts/ChartsView.vue’ import Counter from “../views/counter/Counter.vue”; import SubjectsList from “../views/subjects/SubjectsList.vue”; import ClassesList from “../views/classes/ClassesList.vue”; import ClassesAdd from “../views/classes/ClassesAdd.vue”; import CoursesList from “../views/courses/CoursesList.vue”; import CoursesAdd from “../views/courses/CoursesAdd.vue”; import TeachersList from “../views/teachers/teachersList.vue”; import TeachersAdd from “../views/teachers/teachersAdd.vue”; import store from ‘@/store’; import router from ‘@/router’;

// 所有动态子路由 const fullRoutes = [ { path: ‘’, name: ‘default’, component: ChartsView }, { path: ‘studentsList’, name: ‘StudentsList’, component: StudentsList, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘学生列表’ } }, { path: ‘studentsAdd’, name: ‘StudentsAdd’, component: StudentsAdd, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘新增学生’ } }, { path: ‘studentsUpdate’, name: ‘StudentsUpdate’, // props: true, component: StudentsUpdate, meta: { notKeepAlive: true, title: ‘学生管理’, subTitle: ‘修改学生’ } }, { path: ‘classesList’, name: ‘ClassesList’, component: ClassesList, meta: { title: ‘班级管理’, subTitle: ‘班级列表’ } }, { path: ‘classesAdd’, name: ‘ClassesAdd’, component: ClassesAdd, meta: { title: ‘班级管理’, subTitle: ‘新增班级’ } }, { path: ‘subjectsList’, name: ‘SubjectsList’, component: SubjectsList, meta: { title: ‘专业管理’, subTitle: ‘专业列表’ } }, { path: ‘coursesList’, name: ‘CoursesList’, component: CoursesList, meta: { notKeepAlive: true, title: ‘课程管理’, subTitle: ‘课程列表’ } }, { path: ‘coursesAdd’, name: ‘CoursesAdd’, component: CoursesAdd, meta: { title: ‘课程管理’, subTitle: ‘新增课程’ } }, { path: ‘teachersList’, name: ‘TeachersList’, component: TeachersList, meta: { title: ‘教师管理’, subTitle: ‘教师列表’ } }, { path: ‘teachersAdd’, name: ‘TeachersAdd’, component: TeachersAdd, meta: { title: ‘教师管理’, subTitle: ‘新增教师’ } }, { path: ‘counter’, name: ‘Counter’, component: Counter, meta: { title: ‘其他功能’, subTitle: ‘计数器’ } } ] export default async () => { // 首次进入时获取动态路由 if (store.state.authMenus.length === 0) { // 发送请求获取菜单并保存在仓库中 await store.dispatch(‘getAuthMenusAsync’) console.log(store.state.authMenus); const authRoutes = fullRoutes.filter(item => store.state.authMenus.includes(item.path)); console.log(authRoutes); // 添加动态路由 router.addRoute({ path: ‘/home’, component: HomeView, children: authRoutes }) router.addRoute({ path: ‘*’, component: () => import(‘@/views/errors/NotFound.vue’) }) } } ```