功能权限使用经典的RBAC模型,shiro做为实现框架。
经典模型可以参见:权限系统与RBAC模型概述[绝对经典],这里引用这篇文章中的几个图,描述下表结构的设计:
表结构的设计就是参考上图,但去掉了操作表,直接冗余到权限表中,因为用到的操作类型有限,仅“有和无”两种,如果类似文件“读、写、执行”等多种操作,就需要操作表了。
每种资源(如菜单、部门)赋与权限,权限分配给角色,角色可分配给用户或用户组,都是一对多的关系。
功能权限仅仅解决了“有和无”的问题,如用户“是否”能看到一个菜单,“是否”能操作一个按钮,“是否”能查看一个部门的数据,但如果数据是部分有权限的,是处理不了的,如虽然用户A与B都可以看到“用户管理”菜单,但分别只能查看自己有权限查看的部门数据,甚至同一条用户数据,A可以查看所有,但B不能查看密码等敏感数据。
最终代码中使用了参考工程若依中的方式,使用自定义Annotation的方式,对需要数据权限操作的函数,手写sql添加到查询条件中,限制用户只能访问自已所拥有角色对应有权限的数据。
其实这样就相当于将权限筛选交给了数据库,逻辑清晰简单,就是sql都变得有点复杂。
上代码,自定义一个Annotation,有两个参数
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,内部其实实现了权限处理,不过如上面所说,做了一些改动)
// 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)
// 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
}