简账 - Vue动态路由及SpringSecurity权限控制

前言

在实现动态路由和权限控制之前,可先了解一下以下三个概念:

什么是动态路由?

现在的大部分项目都是前后端分离的,这就导致了之前后端SpringMVC做的路由跳转交给了前端来控制。动态路由就是前端根据后端返回的路由信息生成权限路由列表

什么是权限控制?

权限控制就是指在用户发起调用接口的请求后,判断该用户是否有相应的权限,然后选择放行或拦截

动态路由和权限控制有什么关系?

用户发起的请求都是从某个路由指向的的页面中发起的,所以权限和路由是一个多对一的关系,即一个路由下面会有多个权限

例如列表页 /list 中有 查询编辑 两种操作,当用户A发起查询列表的请求时,后端就可以判断用户A是否拥有列表页 /list 中的查询权限。

往期链接


一、思路分析

实现的话主要分为分为两个部分:

  1. 后端生成根据用户的角色生成路由权限信息
  2. 前端根据返回的路由权限信息生成路由信息表

简账中后端返回的路由权限信息如下所示:

image.png

因为前端需要根据树形结构的数据来构建权限路由表,所以在后端需要将取到的列表数据转为树形结构返回给前端

tips:修改AntDesginPro中的路由守卫我真的时改了好久!

二、实现方案

数据表设计

路由权限表主要就是要包含两个信息:路由地址权限字符

简账中的路由权限表设计如下所示(tb_menu):

image.png

后端生成路由权限信息

Controller层

    @LoginRequired
    @ApiOperation(value = "获取用户的角色及菜单")
    @GetMapping("/roleMenus")
    public Result<?> getRoleMenus() {
        // 用户角色
        List<RoleDO> roleDOS = roleService.getByUserId(LocalUserId.get());
        // 用户菜单权限
        List<MenuDO> menuDOS = menuService.getUserMenus(LocalUserId.get());
        List<MenuBO> menuBOS =  menuService.copyFromMenuDos(menuDOS);
        List<MenuBO> tree = menuService.generatorMenuTree(menuBOS);
        RoleMenuVO roleMenuVO = new RoleMenuVO(LocalUser.get(), roleDOS, tree);
        return Result.success(roleMenuVO);
    }

Service层中将列表转为Tree


    @Override
    public List<MenuBO> generatorMenuTree(List<MenuBO> boList) {
        return generatorTree(boList);
    }

    private List<MenuBO> generatorTree(List<MenuBO> voList) {
        List<MenuBO> tree = list2Tree(voList, null);
        sortTree(tree);
        return tree;
    }
    
    /**
     * list转为树结构
     */
    private List<MenuBO> list2Tree(List<MenuBO> list, Integer pId) {
        List<MenuBO> tree = new ArrayList<>();
        Iterator<MenuBO> it = list.iterator();
        while (it.hasNext()) {
            MenuBO m = it.next();
            if (m.getParentId() == pId) {
                tree.add(m);
                // 已添加的元素删除掉
                it.remove();
            }
        }
        // 寻找子元素
        tree.forEach(n -> n.setChildren(list2Tree(list, n.getId())));
        return tree;
    }

前端生成权限路由列表

生成动态路由的主要代码

注意:在这里权限的标识是放到路由的meta中

/**
 * 动态生成菜单
 * @returns {Promise<Router>}
 */
export const generatorDynamicRouter = (ret) => {
  return new Promise((resolve, reject) => {
    const menuNav = []
    rootRouter.children = ret
    menuNav.push(rootRouter)
    const routers = generator(menuNav)
    routers.push(notFoundRouter)
    resolve(routers)
  })
}

/**
 * 格式化树形结构数据 生成 vue-router 层级路由表
 *
 * @param routerMap
 * @param parent
 * @returns {*}
 */
export const generator = (routerMap, parent) => {
  const children = []
  routerMap.forEach(item => {
    // 如果为按钮,则终止此次循环
    if (item.menuType !== 'F') {
      // 如果为菜单,则找到孩子节点的所有权限,并组成数组
      if (item.menuType === 'C' && item.children) {
        const arr = findMenuPermissions(item.children)
        if (arr.length > 0) {
          item.permissionSign = arr
        }
      }
      const path = item.outerChain ? `${item.path}` : `${parent && parent.path !== '/' && parent.path || ''}/${item.path === '/' ? '' : item.path}`
      // 判断是否为目录
      if (item.menuType === 'M') {
        item.component = 'RouteView'
      }
      const currentRouter = {
        //  动态拼接路由地址
        path: path,
        // 路由名称
        name: item.menuName,
        // 动态加载该路由对应页面的组件
        component: item.outerChain ? undefined : (constantRouterComponents[item.component]) || (() => import(`@/views${parent && parent.path !== '/' && parent.path || ''}/${item.component}`)),
        meta: {
          title: item.menuTitle,
          icon: item.iconName || undefined,
          hiddenHeaderContent: false,
          target: item.outerChain ? '_blank' : undefined,
          permission: item.permissionSign
        }
      }
      // 重定向
      // item.redirect && (currentRouter.redirect = item.redirect)
      // 如当前节点为home则修改父节点的redirect
      if (currentRouter.path === '/home') {
        parent.redirect = currentRouter.path
      }
      if (item.outerChain === 1) {
        currentRouter.redirect = item.path
      }
      // 是否有子菜单,并递归处理
      if (item.children && item.children.length > 0) {
        // Recursion
        const t = generator(item.children, currentRouter)
        // 如果没有孩子,则不赋值
        if (t.length > 0) currentRouter.children = t
      }
      children.push(currentRouter)
    }
  })
  return children
}

前端判断某个按钮是否有权限

/**
 * Action 权限指令
 * 指令用法:
 *  - 在需要控制 action 级别权限的组件上使用 v-action:[method] , 如下:
 *    <i-button v-action:add >添加用户</a-button>
 *    <a-button v-action:delete>删除用户</a-button>
 *    <a v-action:edit @click="edit(record)">修改</a>
 *
 *  - 当前用户没有权限时,组件上使用了该指令则会被隐藏
 *  - 当后台权限跟 pro 提供的模式不同时,只需要针对这里的权限过滤进行修改即可
 *
 *  tips:此处已根据后台返回值做了修改
 */
const action = Vue.directive('action', {
  inserted: function (el, binding, vnode) {
    const actionName = binding.arg
    // 当前用户菜单列表,含权限
    const permissions = vnode.context.$route.meta.permission
    let hasPermission = false
    for (let i = 0; i < permissions.length; i++) {
      if (permissions[i] && permissions[i] === actionName) {
        hasPermission = true
        break
      }
    }
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el) || (el.style.display = 'none')
    }
  }
})

结果

管理员用户的菜单路由如下所示:

image.png

普通用户的菜单路由如下所示:

image.png

测试

这里假设前端忘记通过权限字符来隐藏编辑的按钮了(正常情况下,如无编辑权限此按钮不会展示)

这里测试人员是没有新建用户的权限的

image.png

当点击确认添加用户后会提示权限不足,说明此功能正常

image.png

三、总结

以上代码均可在简账后端简账PC端中找到,如有兴趣可至仓库查看。

感谢看到最后,非常荣幸能够帮助到你~❤❤❤❤

  • 6
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值