demo

scenario

Background in the early development of a management system, function involves various departments (product, customer service, market, etc.), in the first version, I spend shorts and back-end with the hand hand series access plan, the early is very nice, but slowly increased as the function, the business is more and more complex, becomes some hard, because our permissions dynamics is too big

  1. There is a clear division of authority in the hand touch series permission scheme, while the job responsibilities of our company department are sometimes vague.
  2. The back-end uses the RBAC permission scheme. In order to meet the requirements of Point 1, the roles are divided very carefully, and the roles sometimes change frequently, resulting in manual maintenance of the front-end every time

In order to solve the above two pain points, I made a transformation of the original plan.

  1. The front end no longer controls permissions in terms of roles, but more granular operations (interfaces), meaning that the front end does not care about roles
  2. Routing is still maintained by the front end (our back end is very averse to maintaining anything that doesn’t belong to them 😂), but instead filters permission routing through action lists
  3. Use a single (easy to maintain) way to control local permissions on a page, no longer using custom directives, but through functional components, because of the overhead of using custom directives (insert and remove)

Backend coordination:

  1. Provides an interface to get a list of current user actions
  2. A unique identifier (opcode) must be added to the operation list for front-end use, unchanged
  3. An operation list needs to be addedrouterNameField for visual permission editing

A few caveats:

  1. Such as A list of permissions page A, at the same time the list interface used by permissions page B, now you configure permissions to A user without A page limits, but you can use the B page, if you were carried out to all the functions can use page B, then there will be A problem, so try not to access interface across pages use, You need to distinguish which data needs to be retrieved through the dictionary interface versus the permission interface
  2. Some people may struggle, front-end maintenance authority security? For sure, it is not safe. Security is mainly controlled at the back end. The back end does permissions control on data and interfaces, and the front end does permissions control, I think, mainly for interactive experience. Why did you let me see that shit without clearance?
  3. Before using this approach, make sure that this is really necessary in the current scenario, since you have a constant battle with opcodes in the case of large projects with many interfaces

implementation

Example Action List

The following uses Restful interfaces as an example

const operations = [
  {
    url: '/xxx'.type: 'get'.name: 'query XXX'.routeName: 'route1'.// The route corresponding to the interface
    opcode: 'XXX_GET' // Opcode, invariant
  },
  {
    url: '/xxx'.type: 'post'.name: 'new XXX'.routeName: 'route1'.opcode: 'XXX_POST'
  },
  / /...
]
Copy the code

Route changes

Add a configuration field, such as requireOps, to the meta of the route. The value may be String or Array, which indicates the necessary opcodes to be displayed on the current route page. The Array type is used to deal with the situation that a route page needs to be displayed when multiple operation permissions exist at the same time. If the value is not one of the two, the permission control is not enabled and any user can access it

As the menus will eventually need to be dynamically generated based on filtered permission routes, several fields need to be added to the routing options to deal with display issues, where hidden has a higher priority than Visible

  1. hiddenWhen the value is true, routes and child routes will not appear in the menu
  2. visibleWhen the value is false, routes are not displayed, but child routes are displayed
const permissionRoutes = [
  {
    // visible: false,
    // hidden: true,
    path: '/xxx'.name: 'route1'.meta: {
      title: 'route 1'.requireOps: 'XXX_GET'
    },
    // ...}]Copy the code

Because the route is maintained in the front end, the above configuration can only be written to death. If the back end can agree to maintain this routing table, it can have a lot of space to play, and the experience can be better.

Permission Route Filtering

Normalize the permission routing while keeping a copy that may be needed for visualization

const routeMap = (routes, cb) = > routes.map(route= > {
  if (route.children && route.children.length > 0) {
    route.children = routeMap(route.children, cb)
  }
  return cb(route)
})
const hasRequireOps = ops= > Array.isArray(ops) || typeof ops === 'string'
const normalizeRequireOps = ops= >hasRequireOps(ops) ? [].concat(... [ops]) :null
const normalizeRouteMeta = route= > {
  constmeta = route.meta = { ... (route.meta || {}) } meta.requireOps = normalizeRequireOps(meta.requireOps)return route
}

permissionRoutes = routeMap(permissionRoutes, normalizeRouteMeta)
const permissionRoutesCopy = JSON.parse(JSON.stringify(permissionRoutes))
Copy the code

Once you have the action list, you just need to iterate through the permission route and then check to see if the action represented by requireOps is in the action list. If there are permission routes in all child routes, the requireOps value should be automatically added to the parent route. Otherwise, when all child routes do not have permission, the parent route is considered to be without permission control and accessible. If only one child route has no permission control, there is no need to deal with the parent route. So this can be done recursively, dealing with the child route first and then the parent route

const filterPermissionRoutes = (routes, cb) = > {
  // There may be no requireOps set for the parent route
  routes.forEach(route= > {
    if (route.children) {
      route.children = filterPermissionRoutes(route.children, cb)
      
      if(! route.meta.requireOps) {const hasNoPermission = route.children.some(child= > child.meta.requireOps === null)
        // If there is a child route that does not require permission control, the child route is skipped
        if(! hasNoPermission) { route.meta.requireOps = [].concat(... route.children.map(child= > child.meta.requireOps))
        }
      }
    }
  })

  return cb(routes)
}
Copy the code

Then the permission routes are filtered according to the operation list

let operations = null // Update it after getting it from the back end
const hasOp = opcode= > operations
  ? operations.some(op= > op.opcode === opcode)
  : false

const proutes = filterPermissionRoutes(permissionRoutes, routes => routes.filter(route= > {
  const requireOps = route.meta.requireOps

  if (requireOps) {
    return requireOps.some(hasOp)
  }

  return true
}))

// Dynamically add routes
router.addRoutes(proutes)
Copy the code

Functional components control local permissions

This component implementation is very simple, based on the opcode passed to determine the permissions, if the slot content is returned, otherwise return NULL. In addition, for consistency, the root attribute is supported, representing the root node of the component

const AccessControl = {
  functional: true,
  render (h, { data, children }) {
    const attrs = data.attrs || {}

    // In the case of root, direct transparent transmission
    if(attrs.root ! = =undefined) {
      return h(attrs.root || 'div', data, children)
    }

    if(! attrs.opcode) {return h('span', {
        style: {
          color: 'red'.fontSize: '30px'}},'Please configure opcode')}const opcodes = attrs.opcode.split(', ')

    if (opcodes.some(hasOp)) {
      return children
    }

    return null}}Copy the code

Dynamically generate permission menus

Taking ElementUI as an example, since dynamic rendering requires recursion, there is an extra layer of root components in the form of file components, so here is a simple example using the Render Function, which can be modified to suit your needs

// Permissions menu component
export const PermissionMenuTree = {
  name: 'MenuTree'.props: {
    routes: {
      type: Array.required: true
    },
    collapse: Boolean
  },
  render (h) {
    const createMenuTree = (routes, parentPath = ' ') = > routes.map(route= > {
      // Hidden: True when the current menu and submenus are not displayed
      if (route.hidden === true) {
        return null
      }

      // subpath processing
      const fullPath = route.path.charAt(0) = = ='/' ? route.path : `${parentPath}/${route.path}`

      // visible: if false, the current menu is not displayed, but submenus are displayed
      if (route.visible === false) {
        return createMenuTree(route.children, fullPath)
      }

      const title = route.meta.title
      const props = {
        index: fullPath,
        key: route.path
      }

      if(! route.children || route.children.length ===0) {
        return h(
          'el-menu-item',
          { props },
          [h('span', title)]
        )
      }

      return h(
        'el-submenu',
        { props },
        [
          h('span', { slot: 'title'}, title), ... createMenuTree(route.children, fullPath) ] ) })return h(
      'el-menu',
      {
        props: {
          collapse: this.collapse,
          router: true.defaultActive: this.$route.path
        }
      },
      createMenuTree(this.routes)
    )
  }
}
Copy the code

Interface permission control

We usually use Axios, and all we need to do here is add a few lines of code to the axios wrapper, which is a lot of stuff, so here’s a simple example

const ajax = axios.create(/* config */)

export default {
  post (url, data, opcode, config = {}) {
    if(opcode && ! hasOp(opcode)) {return Promise.reject(new Error('No operation permission'))}return ajax.post(url, data, { /* config */. config }).then(({ data }) = > data)
  },
  // ...
}
Copy the code

At this point, the scheme is almost complete. The visualization of permission configuration can be done according to routeName in the action list, and the operation and permission routing can be mapped one by one. There is a simple implementation in the demo

reference

Hand to hand, take you to use vUE masturbation background series two (login permission)