KKing权限模型

文章目录

    • 一、功能权限
    • 二、数据权限
    • 三、后台实现
    • 四、前后分离

KKing是一个Springboot+Ant-Design-Pro-Vue实现的前后端分离Admin权限管理系统, 预览地址,源码在 Github,欢迎star

一、功能权限

       功能权限使用经典的RBAC模型,shiro做为实现框架。
       经典模型可以参见:权限系统与RBAC模型概述[绝对经典],这里引用这篇文章中的几个图,描述下表结构的设计:

KKing权限模型_第1张图片

RBAC0模型

KKing权限模型_第2张图片

参考文献中的一种设计

       表结构的设计就是参考上图,但去掉了操作表,直接冗余到权限表中,因为用到的操作类型有限,仅“有和无”两种,如果类似文件“读、写、执行”等多种操作,就需要操作表了。
       每种资源(如菜单、部门)赋与权限,权限分配给角色,角色可分配给用户或用户组,都是一对多的关系。

二、数据权限

       功能权限仅仅解决了“有和无”的问题,如用户“是否”能看到一个菜单,“是否”能操作一个按钮,“是否”能查看一个部门的数据,但如果数据是部分有权限的,是处理不了的,如虽然用户A与B都可以看到“用户管理”菜单,但分别只能查看自己有权限查看的部门数据,甚至同一条用户数据,A可以查看所有,但B不能查看密码等敏感数据。

三、后台实现

       最终代码中使用了参考工程若依中的方式,使用自定义Annotation的方式,对需要数据权限操作的函数,手写sql添加到查询条件中,限制用户只能访问自已所拥有角色对应有权限的数据。
       其实这样就相当于将权限筛选交给了数据库,逻辑清晰简单,就是sql都变得有点复杂。
       上代码,自定义一个Annotation,有两个参数

  • 类型:现在代码中有两种,一种是部门,一种是菜单,分别有自己的过滤实现
  • 表别名:sql中需要过滤的表的别名,因为仅用字段名有可能会有歧义
public @interface DataScope {
    public String type() default "dept";
    public String tableAlias() default "";
}

@Override
@DataScope(type = "menu")
public List<JSONObject> getUserMenu(TSysMenu tSysMenu) {
    List<TSysMenu> menuList = tSysMenuMapper.getUserMenu(tSysMenu);
    return TreeUtil.toTreeList(menuList);
}

写一个切面,针对这个注解,针对不同类型,做数据权限过滤,逻辑是根据用户所拥有的权限进行过滤,拿菜单做一个例子

@Pointcut("@annotation(com.kking.common.annotation.DataScope)")
public void dataScopePointCut(){}

private void handleMenuDataScope(JoinPoint jp){
    TSysUser user = (TSysUser)SecurityUtils.getSubject().getPrincipal();
    BaseEntity entity = (BaseEntity)jp.getArgs()[0];

    String sql = String.format(
                "and m.id in (" +
                    "select p.resource_id " +
                    "from t_sys_user_role ur,t_sys_role_perm rp,t_sys_perm p " +
                    "where ur.role_id=rp.role_id and rp.perm_id=p.id " +
                    "and ur.user_id=%d and p.perm_type='MENU')",user.getId());

    //给entity添加额外sql
    entity.getParams().put("dataScope",sql);
}

最后,在mybatis的xml文件中,将这个额外的数据权限sql添加到需要做数据权限过滤的sql中,如:

<delete id="deleteById" parameterType="TSysMenu">
    update t_sys_menu m set state=1 where id=#{id}
    ${params.dataScope}
delete>

这样,最后需要有数据权限过滤的sql,都变成了这个样子:

update t_sys_dept set state=1 where id=? 
and id in (
    select id from (
        select distinct d.id 
        from t_sys_user_role ur,t_sys_role_perm rp,t_sys_perm p,t_sys_dept d 
        where ur.user_id=1 and rp.role_id=ur.role_id and p.id=rp.perm_id and FIND_IN_SET(p.resource_id,concat(d.id,',',d.pids)) and p.perm_type='DEPT'
    ) a
) 

四、前后分离

       前后分离,所有数据通过ajax来走接口,权限控制也类似,后端传递权限数据给前端,前端来判断菜单、按钮等是否展示,当然后端接口也要加上权限控制。
       实现KKing时,直接偷了个懒,直接菜单都走配置化,所有菜单数据配置好后,由后端过滤权限后,传递可展示菜单及权限列表给前端,前端菜单通过动态路由添加上去,界面通过权限来控制是否展示,后端过滤权限比较简单,不详细展开,稍微说一下前端的实现。(前端直接使用的是ant-design-pro-vue,内部其实实现了权限处理,不过如上面所说,做了一些改动)

  1. 后端返回的数据由原来的只有权限数据,变成添加菜单数据;
  2. 原来前端是根据权限数据来进行过滤菜单展示,现在变成根据菜单直接添加路由
// permission.js
// 新增服务器返回的菜单路由
const makeRoutesSafe = (routes, deep) => {
  for (var index in routes) {
    var route = routes[index]
    var meta = { title: route.name, icon: route.icon }
    route.meta = meta
    if (!route.path) {
      route.path = ''
    }
    if (route.children) {
      route.component = PageView
    } else {
      const componentPath = route.component
      // 动态加载组件
      route.component = () => import(`@/views/${componentPath}`)
    }
    if (route.children) {
      makeRoutesSafe(route.children, ++deep)
    }
  }
  return routes
}
export const addRoutes = (newRoutes) => {
  makeRoutesSafe(newRoutes, 0)
  store.commit('SET_ROUTERS', newRoutes)
  router.addRoutes(store.getters.addRouters)
}
...
addRoutes(res.menus)
  1. 修改v-action为v-perm指令,直接根据权限标识判断组件显示;
  2. 添加Vue.prototype.$hasPerm,用于jsx中判断权限
// permission.js
/**
 * Perm 权限指令
 * 指令用法:
 *  - 在需要控制 perm 级别权限的组件上使用 v-perm:[method] , 如下:
 *    添加用户
 *    删除用户
 *    修改
 *  - jsx中请使用如下方式
 *    {this.$hasPerm('user:add')?添加用户:''}
 *
 *  - 当前用户没有权限时,组件上使用了该指令则会被隐藏
 */
function hasPerm (permName) {
  const roles = store.getters.roles
  var permList = []
  roles.map(role => {
    permList.push(...role.permissionList)
  })
  return permList.indexOf(permName) >= 0
}

Vue.prototype.$hasPerm = hasPerm

const perm = Vue.directive('perm', {
  bind: function (el, binding, vnode) {
    const permName = binding.arg

    if (!hasPerm(permName)) {
      setTimeout(() => {
        if (el.parentNode == null) {
          el.style.display = 'none'
        } else {
          el.parentNode.removeChild(el)
        }
      }, 10)
    }
  }
})

export {
  perm
}

你可能感兴趣的:(Java,SpringBoot,KKing)