Как элегантно добавить контроль разрешений в vue

Vue.js

предисловие

В проекте некоторые функции будут связаны с важным управлением данными.Для обеспечения безопасности данных мы добавим разрешения в проект, чтобы ограничить операции каждого пользователя. Что касается внешнего интерфейса, то нам нужно сотрудничать с данными разрешений, предоставленными серверной частью, чтобы накладывать различные ограничения на страницу.

необходимость

Поскольку это бизнес-требование на работе, для меня в основном есть два места, где требуется контроль разрешений.

Первая — это боковая панель меню, которую нужно отображать и скрывать.

Второе — это кнопки, всплывающие окна и т. д. на странице.

Процесс

  1. Как получить права пользователя?

    Back-end (список разрешений, принадлежащих текущему пользователю) -> front-end (полученный через back-end интерфейс, здесь и далее мы называем список разрешений текущего пользователя permissionList)

  2. Как ограничить переднюю часть?

    В соответствии с требованиями продукта настройте точки разрешений в проекте, а затем с помощью списка разрешений узнайте, есть ли настроенные точки разрешений.

  3. Тогда что?

    Ушел.

Вот о чем я подумал, когда впервые получил этот запрос, что в нем такого сложного, просто получите PermissionList и судите по нему. Только позже я обнаружил, что настоящие потребности гораздо сложнее, чем я думал.

реальная проблема

В приведенных выше требованиях упоминалось, что мы в основном решаем две проблемы: отображение боковой панели меню и работа на странице.

Предположим, у нас есть такая настройка маршрутизации (далее приведен только пример):

import VueRouter from 'vue-router'
/* 注意:以下配置仅为部分配置,并且省去了 component 的配置 */
export const routes = [
  {
    path: '/',
    name: 'Admin',
    label: '首页'
  },
  {
    path: '/user',
    name: 'User',
    label: '用户',
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用户列表'
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用户组',
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用户组列表'
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用户组设置'
          }
        ]
      }
    ]
  },
  {
    path: '/setting',
    name: 'Setting',
    label: '系统设置'
  },
  {
    path: '/login',
    name: 'Login',
    label: '登录'
  }
]

const router = new VueRouter({
  routes
})

export default router

Первые два уровня маршрутизации будут отображаться на боковой панели, а третий уровень не будет отображаться на боковой панели.

Настройки разрешений для операций на странице не нужно рассматривать много других вещей.Мы в основном разбираем проблемы сайдбара и роутинга.Посредством анализа в основном выявляются следующие проблемы:

  1. Когда получать список разрешений и как хранить список разрешений
  2. Подмаршруты не должны отображаться, если у них вообще нет разрешений (например: пользователи не должны отображаться на боковой панели, когда ни список пользователей, ни группа пользователей не имеют разрешений)
  3. Когда перенаправленный маршрут по умолчанию не имеет разрешения, вы должны искать перенаправление с разрешением в дочерних элементах (например: маршрут пользователя перенаправляется на маршрут списка пользователей, если список пользователей не имеет разрешения, он должен быть перенаправлен на маршрут группы пользователей)
  4. Когда пользователь напрямую вводит неавторизованный URL-адрес, ему необходимо перейти на неавторизованную страницу или выполнить другие операции. (ограничения маршрутизации)

Ниже мы решаем вышеуказанные проблемы одну за другой.

Когда получать разрешения, где хранить и ограничения маршрутизации

я здесьrouterизbeforeEachПолученный список разрешений сохраняется вvuexсередина.

Причина в том, чтобы учесть ограничения на маршрутизацию и упростить использование списков разрешений в последующих проектах.Ниже приведен пример реализации:

Сначала мы добавляем конфигурацию разрешений на маршрутизатор:

// 以下只展示部分配置
{
  path: '/user',
  name: 'User',
  label: '用户',
  meta: {
    permissions: ['U_1']
  },
  redirect: { name: 'UserList' },
  children: [
    {
      path: 'list',
      name: 'UserList',
      label: '用户列表',
      meta: {
        permissions: ['U_1_1']
      }
    },
    {
      path: 'group',
      name: 'UserGroup',
      label: '用户组',
      meta: {
        permissions: ['U_1_2']
      },
      redirect: { name: 'UserGroupList' },
      children: [
        {
          path: 'list',
          name: 'UserGroupList',
          label: '用户组列表',
          meta: {
            permissions: ['U_1_2_1']
          }
        },
        {
          path: 'config',
          name: 'UserGroupConfig',
          label: '用户组设置',
          meta: {
            permissions: ['U_1_2_2']
          }
        }
      ]
    }
  ]
}

Вы можете видеть, что мы добавили права доступа к метаданным, чтобы упростить доступ кrouter.beforeEchРазрешения оцениваются в , а разрешения задаются в виде массива, поскольку страница может включать несколько разрешений.

Далее мы устанавливаемrouter.beforeEach :

// 引入项目的 vuex
import store from '@/store'
// 引入判断是否拥有权限的函数
import { includePermission } from '@/utils/permission'

router.beforeEach(async (to, from, next) => {
  // 先判断是否为登录,登录了才能获取到权限,怎么判断登录就不写了
  if (!isLogin) {
    try {
      // 这里获取 permissionList
      await store.dispatch('getPermissionList')
      // 这里判断当前页面是否有权限
      const { permissions } = to.meta
      if (permissions) {
        const hasPermission = includePermission(permissions)
        if (!hasPermission) next({ name: 'NoPermission' })
      }
      next()
    }
  } else {
    next({ name: 'Login' })
  }
})

Мы видим, что нам нужен метод для оценки разрешений, и getPermissionList в vuex выглядит следующим образом:

// @/store
export default {
  state: {
    permissionList: []
  },
  mutations: {
    updatePermissionList: (state, payload) => {
      state.permissionList = payload
    }
  },
  actions: {
    getPermissionList: async ({ state, commit }) => {
      // 这里是为了防止重复获取
      if (state.permissionList.length) return
      // 发送请求方法省略
      const list = await api.getPermissionList()
      commit('updatePermissionList', list)
    }
  }
}
// @/utils/permission
import store from '@/store'

/**
 * 判断是否拥有权限
 * @param {Array<string>} permissions - 要判断的权限列表
 */
function includePermission (permissions = []) {
  // 这里要判断的权限没有设置的话,就等于不需要权限,直接返回 true
  if (!permissions.length) return true
  const permissionList = store.state.permissionList
  return !!permissions.find(permission => permissionList.includes(permission))
}

проблема перенаправления

Выше мы разобрались с базовой настройкой роутинга и как получить разрешения, и как ограничить роутинговые переходы, Следующее, с чем нам нужно разобраться, это проблема перенаправления.

Это может быть связано со структурой самого нашего проекта. Под боковой панелью нашего проекта есть подуровни, которые отображаются с помощью переключателя вкладок на следующем рисунке. В обычных условиях, когда вы нажимаете на управление лекарствами, страница быть перенаправлены на вкладку переключения страницы управления хранением, но когда входящее управление не имеет разрешения, оно должно быть перенаправлено непосредственно в исходящий интерфейс управления.

Таким образом, для достижения вышеуказанного эффекта мне нужно переписать перенаправление маршрутизатора, чтобы его можно было оценивать динамически (потому что я не знаю список разрешений текущего пользователя при настройке маршрута).

Затем я проверил документацию vue-router и обнаружил, что перенаправление может быть методом, позволяющим решить проблему перенаправления.

описание перенаправления в vue-router, по инструкции можем переписать редирект следующим образом:

// 我们需要引入判断权限方法
import { includePermission } from '@/utils/permission'

const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用户列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用户组',
    meta: {
      permissions: ['U_1_2']
    }
  }
]

const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用户',
  redirect: (to) => {
    if (includePermission(children[0].meta.permissions)) return { name: children[0].name }
    if (includePermission(children[1].meta.permissions)) return { name: children[1].name }
  },
  children
}

Хоть проблема и решаема, но писать так очень хлопотно, да и конфигурацию роутера нужно модифицировать, поэтому используем метод для генерации:

// @/utils/permission
/**
 * 创建重定向函数
 * @param {Object} redirect - 重定向对象
 * @param {string} redirect.name - 重定向的组件名称
 * @param {Array<any>} children - 子列表
 */
function createRedirectFn (redirect = {}, children = []) {
  // 避免缓存太大,只保留 children 的 name 和 permissions
  const permissionChildren = children.map(({ name = '', meta: { permissions = [] } = {} }) => ({ name, permissions }))
  return function (to) {
    // 这里一定不能在 return 的函数外面筛选,因为权限是异步获取的
    const hasPermissionChildren = permissionChildren.filter(item => includePermission(item.permissions))
    // 默认填写的重定向的 name
    const defaultName = redirect.name || ''
    // 如果默认重定向没有权限,则从 children 中选择第一个有权限的路由做重定向
    const firstPermissionName = (hasPermissionChildren[0] || { name: '' }).name
    // 判断是否需要修改默认的重定向
    const saveDefaultName = !!hasPermissionChildren.find(item => item.name === defaultName && defaultName)
    if (saveDefaultName) return { name: defaultName }
    else return firstPermissionName ? { name: firstPermissionName } : redirect
  }
}

Тогда мы можем переписать это как:

// 我们需要引入判断权限方法
import { includePermission, createRedirectFn } from '@/utils/permission'

const children = [
  {
    path: 'list',
    name: 'UserList',
    label: '用户列表',
    meta: {
      permissions: ['U_1_1']
    }
  },
  {
    path: 'group',
    name: 'UserGroup',
    label: '用户组',
    meta: {
      permissions: ['U_1_2']
    }
  }
]

const routeDemo = {
  path: '/user',
  name: 'User',
  label: '用户',
  redirect: createRedirectFn({ name: 'UserList' }, children),
  children
}

Это немного более лаконично, но мне все еще нужно изменить один маршрут за другим, поэтому я написал еще один метод для рекурсивной настройки маршрутизаторов и переписывания их перенаправлений:

// @/utils/permission
/**
 * 创建有权限的路由配置(多级)
 * @param {Object} config - 路由配置对象
 * @param {Object} config.redirect - 必须是 children 中的一个,并且使用 name
 */
function createPermissionRouter ({ redirect, children = [], ...others }) {
  const needRecursion = !!children.length
  if (needRecursion) {
    return {
      ...others,
      redirect: createRedirectFn(redirect, children),
      children: children.map(item => createPermissionRouter(item))
    }
  } else {
    return {
      ...others,
      redirect
    }
  }
}

Таким образом, нам нужно только добавить такой уровень функций в конфигурацию самого внешнего маршрутизатора:

import { createPermissionRouter } from '@/utils/permission'

const routesConfig = [
  {
    path: '/user',
    name: 'User',
    label: '用户',
    meta: {
      permissions: ['U_1']
    },
    redirect: { name: 'UserList' },
    children: [
      {
        path: 'list',
        name: 'UserList',
        label: '用户列表',
        meta: {
          permissions: ['U_1_1']
        }
      },
      {
        path: 'group',
        name: 'UserGroup',
        label: '用户组',
        meta: {
          permissions: ['U_1_2']
        },
        redirect: { name: 'UserGroupList' },
        children: [
          {
            path: 'list',
            name: 'UserGroupList',
            label: '用户组列表',
            meta: {
              permissions: ['U_1_2_1']
            }
          },
          {
            path: 'config',
            name: 'UserGroupConfig',
            label: '用户组设置',
            meta: {
              permissions: ['U_1_2_2']
            }
          }
        ]
      }
    ]
  }
]

export const routes = routesConfig.map(item => createPermissionRouter(item))

const router = new VueRouter({
  routes
})

export default router

Конечно, в таком написании есть еще одно преимущество: вам не нужно устанавливать редирект, который будет автоматически перенаправлять на первый авторизованный маршрут дочерних элементов.

Проблема с отображением боковой панели

Наш проект использует конфигурацию маршрута для генерации сайдбара.Конечно, будут добавлены некоторые другие параметры для отображения уровня отображения и других вопросов.Я не буду писать здесь конкретный код.Как решить проблему, которая возникает у детей в боковая панель не отображается без разрешения.проблема.

Моя идея состоит в том, чтобы вместе обновить конфигурацию маршрутизации до vuex, а затем прочитать конфигурацию боковой панели из конфигурации в vuex.

Так как это место требует много модификаций и бизнеса, я не буду выносить код, вы можете поэкспериментировать сами.

Способы облегчить командам развертывание точек доступа

Мы решили большую часть вышеперечисленных проблем с разрешениями, поэтому остается еще много развертываний точек разрешений, связанных с бизнес-логикой, поэтому для того, чтобы другие люди в команде могли элегантно и просто развертывать точки разрешений на каждой странице, я предоставляю следующее в проект Способы развертывания разрешений:

  1. по командеv-permissionустановить прямо на шаблон
<div v-permission="['U_1']"></div>
  1. через глобальный методthis.$permissionСуждение, потому что некоторых разрешений нет в шаблоне
{
  hasPermission () {
    // 通过方法 $permission 判断是否拥有权限
    return this.$permission(['U_1_1', 'U_1_2'])
  }
}

Обратите внимание, что для$permissionВозвращаемое значение метода можно контролировать, и о нем нужно судить поthis.$storeЧтобы судить, следующий код реализации:

// @/utils/permission
/**
 * 判断是否拥有权限
 * @param {Array<string|number>} permissions - 要判断的权限列表
 * @param {Object} permissionList - 传入 store 中的权限列表以实现数据可监测
 */
function includePermissionWithStore (permissions = [], permissionList = []) {
  if (!permissions.length) return true
  return !!permissions.find(permission => permissionList.includes(permission))
}
import { includePermissionWithStore } from '@/utils/permission'
export default {
  install (Vue, options) {
    Vue.prototype.$permission = function (permissions) {
      const permissionList = this.$store.state.permissionList
      return includePermissionWithStore(permissions, permissionList)
    }
  }
}

Ниже приведен код реализации директивы (чтобы не конфликтовать с v-if, отображение элемента управления скрывается добавлением/удалением className):

// @/directive/permission
import { includePermission } from '@/utils/permission'
const permissionHandle = (el, binding) => {
  const permissions = binding.value
  if (!includePermission(permissions)) {
    el.classList.add('hide')
  } else {
    el.classList.remove('hide')
  }
}
export default {
  inserted: permissionHandle,
  update: permissionHandle
}

Суммировать

В ответ на предыдущий вопрос, есть следующее резюме:

  1. Когда получать список разрешений и как хранить список разрешений

    router.beforeEach получает его и сохраняет в vuex.

  2. Подмаршруты не должны отображаться, если у них вообще нет разрешений (например: пользователи не должны отображаться на боковой панели, если ни список пользователей, ни пользовательские настройки не имеют разрешений)

    Сохраняя конфигурацию маршрутизации в vuex, создавайте настройки боковой панели и изменяйте конфигурацию в vuex после получения разрешений на управление отображением и скрытием.

  3. Когда перенаправленный маршрут по умолчанию не имеет разрешения, вы должны искать перенаправление с разрешением в дочерних элементах (например: маршрут пользователя перенаправляется на маршрут списка пользователей, если список пользователей не имеет разрешения, он должен быть перенаправлен на маршрут группы пользователей)

    пройти черезvue-routerсерединаredirectУстановить какFunctionреализовать

  4. Когда пользователь напрямую вводит неавторизованный URL-адрес, ему необходимо перейти на неавторизованную страницу или выполнить другие операции. (ограничения маршрутизации)

    Установите разрешения в meta и оцените разрешения в router.beforeEach.

Выше приведено мое общее решение и реализация кода для этого требования к разрешению. Возможно, оно не идеально, но я все же надеюсь, что оно может вам помочь ^_^