From Design to Implementation: Frontend Permission Development with RBAC

This article walks through the complete process of building a front‑end permission system using Role‑Based Access Control (RBAC), covering scheme selection, permission field design, route generation, dynamic routing, Vue‑router guards, Vuex storage, and common pitfalls with detailed code examples.

IoT Full-Stack Technology
IoT Full-Stack Technology
IoT Full-Stack Technology
From Design to Implementation: Frontend Permission Development with RBAC

The article begins by explaining why permission control is a core requirement for backend management systems and lists several common access control schemes, including Role‑Based Access Control (RBAC), Permission‑Based Access Control, Resource‑Based Access Control, Hierarchical Access Control, and Rule‑Based Access Control. The author selects RBAC for its flexibility and ease of management, describing how roles group permissions and simplify administration.

1. RBAC Permission Field Design

Permissions are defined as strings in the format module:feature:action (e.g., sys:user:edit). The naming convention supports wildcards such as sys:user:*, which should be stored without the asterisk for front‑end safety.

Permission string – identifies the specific operation.

Role – a collection of permission strings.

2. Permission Management Model

The data model consists of users (login, password, role), roles (name, permission strings, menus), menus (name, URL, type), and many‑to‑many relations between users‑roles and roles‑menus. A diagram illustrates the user <--> role <--> menu/permission relationships.

3. Implementation Steps

Login – store the token in sessionStorage.

async login({ commit }, userInfo) {
  const { data } = await login(userInfo)
  const accessToken = 'Bearer ' + data.access_token
  if (accessToken) {
    sessionStorage.setItem('token', accessToken)
  } else {
    message.error(`Login API error, missing token`)
  }
}

Route Guard – verify token, check white‑list, fetch user roles if missing, then load dynamic routes.

router.beforeEach(async (to, from, next) => {
  const hasToken = store.getters['user/accessToken']
  if (hasToken) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      if (!store.getters['user/roles'].length) {
        try {
          await store.dispatch('user/GetInfo')
          await store.dispatch('async-router/GenerateRoutes')
          const accessRoutes = store.getters['async-router/addRouters']
          accessRoutes.forEach(route => {
            if (!isHttp(route.path)) router.addRoute(route)
          })
          next({ ...to, replace: true })
        } catch (e) {
          await store.dispatch('user/Logout')
          next({ path: '/login', query: { redirect: to.fullPath } })
        }
      } else {
        next()
      }
    }
  } else {
    if (routesWhiteList.includes(to.path)) {
      next()
    } else {
      next({ path: '/login', query: { redirect: to.fullPath }, replace: true })
    }
  }
})

Dynamic Route Generation – the back‑end returns a JSON array of private routes. Because JSON cannot carry functions, the front‑end converts the component field into lazy‑load functions.

export const generatorDynamicRouter = (token) => {
  return new Promise((resolve, reject) => {
    getCurrentUserNav()
      .then(res => {
        const menuNav = res.data
        sessionStorage.setItem('generateRoutes', JSON.stringify(menuNav))
        const routers = generator(menuNav)
        resolve(routers)
      })
      .catch(err => reject(err))
  })
}

export const generator = (routerMap, parent) => {
  return routerMap.map(item => {
    const { title, show, hideChildren, hiddenHeaderContent, icon, hidden } = item.meta || {}
    if (item.component) {
      if (item.component === 'Layout') {
        item.component = 'Layout'
      } else if (item.component === 'ParentView') {
        item.component = 'BasicLayout'
        item.path = '/' + item.path
      }
    }
    const currentRouter = {
      path: item.path || `${parent?.path || ''}/${item.key}`,
      name: item.name || item.key || '',
      component: constantRouterComponents[item.component || item.key] || (() => import(`@/views/${item.component}`)).catch(() => import('@/views/404')),
      hidden: item.hidden,
      meta: { title, icon, hiddenHeaderContent, target: validURL(item.path) ? '_blank' : '', permission: item.name, keepAlive: false, hidden },
      fullPath: ''
    }
    if (show === false) currentRouter.hidden = true
    if (!constantRouterComponents[item.component || item.key]) {
      if (parent && parent.path && parent.path !== '/') {
        currentRouter.path = `${parent.path}/${item.path}`
      }
    }
    if (hideChildren) currentRouter.hideChildrenInMenu = true
    if (item.redirect) currentRouter.redirect = item.redirect
    currentRouter.fullPath = currentRouter.path
    if (item.children && item.children.length > 0) {
      currentRouter.children = generator(item.children, currentRouter)
    }
    return currentRouter
  })
}

Storing Routes – after generation, the routes and sidebar menu list are saved in Vuex via actions such as GenerateRoutes, and also cached in sessionStorage to survive page refreshes.

Adding Dynamic Routes – the router uses router.addRoute(route) for each generated route (Vue‑router 4.x replaces the older addRoutes method). A final navigation refresh is performed with next({ ...to, replace: true }) to avoid a white screen.

Common Pitfalls – differences between Vue‑router 3 and 4 (deprecated addRoutes), inability to see newly added routes in devtools, and the need to replace the navigation after adding routes.

Logout & Cleanup – the logout flow clears the token, roles, permissions, and Vuex state, and removes cached data from sessionStorage.

Logout({ commit, dispatch, state }) {
  return new Promise((resolve, reject) => {
    logout()
      .then(async () => {
        await dispatch('resetAll')
        this.dispatch('tagsBar/delAllVisitedRoutes', { root: true })
        resolve()
      })
      .catch(error => reject(error))
  })
}

async resetAll({ dispatch, commit }) {
  await dispatch('setAccessToken', '')
  commit('SET_ROLES', [])
  commit('SET_PERMISSION', [])
  sessionStorage.clear()
}

Overall, the guide demonstrates a complete RBAC front‑end permission solution: selecting RBAC, designing permission strings, modeling user‑role‑menu relationships, implementing login and token handling, converting back‑end route data into Vue‑router compatible objects, dynamically injecting routes, handling navigation guards, persisting state across refreshes, and addressing typical integration issues.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Access ControlVue.jsDynamic RoutingRBACVue RouterFrontend Permissions
IoT Full-Stack Technology
Written by

IoT Full-Stack Technology

Dedicated to sharing IoT cloud services, embedded systems, and mobile client technology, with no spam ads.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.