问题引出
最近,许多学员反馈项目中需要处理数据权限,但是不知道怎么处理比较合适。这篇手记将针对这个问题,给出一种比较通用且容易扩展的数据权限设计方案。
现状
目前流行的权限框架已经有支持数据权限的了,但是需要配置在接口和方法上,扩展性不是很好,那么怎样做能让扩展性最大化呢?
很容易想到的就是:将数据权限的控制放到数据库里存储,在权限拦截时先判断接口是否有权访问,在接口有权访问后,接下来根据配置的条件判断是否有权使用指定的参数值。(做的更高级些,可以对返回的结果进行检查,包含了某个值的某个对象不允许访问的话,也当做无权访问处理,这篇手记里暂时不考虑这个情况)。具体怎么做呢?
数据库设计
先从数据库表设计说起,首先定义一个数据权限控制表结构:
CREATE TABLE `sys_acl_data` (
`id` int(11) NOT NULL,
`acl_id` int(11) NOT NULL COMMENT '对应权限表主键',
`status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态,1:可用,0:不可用',
`param` varchar(20) NOT NULL DEFAULT '' COMMENT '参数',
`operation` int(11) NOT NULL DEFAULT 0 COMMENT '操作类型,0;等于,1:大于,2:小于,3:大于等于,4:小于等于,5:包含,6:介于之间,。。。',
`value1` varchar(100) NOT NULL DEFAULT '0',
`value2` varchar(100) NOT NULL DEFAULT '0',
`next_param_op` int(11) NOT NULL DEFAULT 0 COMMENT '后续有参数时连接的关系,0:没有其他参数控制,1:与,2:或||',
`seq` tinyint(4) NOT NULL DEFAULT '0' COMMENT '顺序',
PRIMARY KEY (`id`),
INDEX `idx_acl_id` USING BTREE (`acl_id`)
) ENGINE=`InnoDB` COMMENT '数据权限表';
具体介绍一下每个字段含义:
主键 id;
acl_id 映射权限点表主键,代表每行记录是针对哪个权限点的;
status 代表当前这条配置是否有效,方便临时激活与禁用;
param 代表需要校验的参数名,允许一个请求有多个参数参与数据校验;如果参数复杂,比如包含对象,定义的参数可能为a.b.c 这种多级的形式,建议不要太复杂
operation 代表数据拦截的规则,使用数字代表是等于、大于、小于、大于等于、小于等于、包含、介于之间等,可以根据自己需要增加或减少支持的拦截规则
value1 和 value2 用来和param、operation组成一个关系表达式,比如:1=a2
next_param_op 字段根据需要使用,如果一个权限点支持多条数据规则时,连接两个规则之间的操作,|| 还是
seq 字段用于某个权限点包含多条数据权限规则时的顺序
假设有这么一条数据,那么他的含义是:id为1(acl_id)的权限点,配置了一条有效(status=1)的数据规则,规则是:传入参数id(param)的值要大于(operation)10(value1)
数据权限校验逻辑
有了表结构后,接下来就是增加接口能对数据进行更新和获取了,然后有个页面能对其进行展示和新增操作了,这里就不占更多篇幅了,重点说一下逻辑的处理。
权限课程里原生实现一套权限管理部分已经对权限点做的基本管理和权限拦截就不在这里重复说了,具体看视频和代码就可以了,这里重点说一下如何在已有的权限上进行数据权限的扩展。首先给出url拦截核心代码和权限校验的核心代码(单独看这段代码不去看课程的细节应该也能看懂个大概):
自定义filter拦截url判断权限核心代码:
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String servletPath = request.getServletPath();
Map requestMap = request.getParameterMap();
if (exclusionUrlSet.contains(servletPath)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
SysUser sysUser = RequestHolder.getCurrentUser();
if (sysUser == null) {
log.warn(someone visit {}, but no login, parameter:{}, servletPath, JsonMapper.obj2String(requestMap));
noAuth(request, response);
return;
}
SysCoreService sysCoreService = ApplicationContextHelper.popBean(SysCoreService.class);
if (!sysCoreService.hasUrlAcl(servletPath)) {
log.warn({} visit {}, but no login, parameter:{}, JsonMapper.obj2String(sysUser), servletPath, JsonMapper.obj2String(requestMap));
noAuth(request, response);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
return;
}
实际判断一个url是否可访问的核心代码:
public boolean hasUrlAcl(String url) {
if (isSuperAdmin()) { // 超级管理员直接允许访问
return true;
}
ListaclList = sysAclMapper.getByUrl(url); // 取出符合条件的权限点
if (CollectionUtils.isEmpty(aclList)) {
return true;
}
ListuserAclList = getCurrentUserAclListFromCache();
SetuserAclIdSet = userAclList.stream().map(acl - acl.getId()).collect(Collectors.toSet());
boolean hasValidAcl = false;
// 规则:只要有一个权限点有权限,那么我们就认为有访问权限
for (SysAcl acl : aclList) { // ----------------------------------------------------- ①
// 判断一个用户是否具有某个权限点的访问权限
if (acl == null || acl.getStatus() != 1) { // 权限点无效
continue;
}
hasValidAcl = true;
if (userAclIdSet.contains(acl.getId())) {
return true; // ------------------------------------------------------ ②
}
}
if (!hasValidAcl) {
return true; // ------------------------------------------------------- ③
}
return false;
}
从代码的 ① 处,可以拿到实际要判断的权限点。在判断某个指定的权限点已经有权限访问时,代码的 ②、③处,需要加入数据权限的校验。
既然要校验参数了,那么就需要把参数传入 hasUrlAcl 这个方法。doFilter方法里的 Map requestMap = request.getParameterMap(); 的requestMap就是url的参数列表,这种方式对于某些特殊的post提交不是完全适用,比如通过body里传递json格式的参数。实际项目中怎么把参数传递到方法里,可以根据项目接口的实际定义来处理。
当hasUrlAcl拿到参数且判断指定的权限点有权访问时,去sys_acl_data表根据acl_id查询出有效的规则列表,逐条判断,这里注意许多细节的处理。1、单条规则的解读,2、多条规则间的逻辑与和或,3、参数带层级时的解读(a.b.c这种),实际中可以根据项目中接口的定义规范来决定处理的复杂度。这个实现后,当url有权访问时,没有数据规则或者数据规则校验通过时,这个url才算真正的有权访问。
这时,肯定有人会问,我的接口是这样定义的/a/{id}.json 这种的该如何做数据权限拦截呢?其实这种方式的接口,课程里目前稍微调整一下也可以支持,调整如下:
SysAclMapper.xml:
select id=getByUrl parameterType=string resultMap=BaseResultMap
SELECT include refid=Base_Column_List /
FROM sys_acl
WHERE url = #{url} !-- url is not null and url != '' and #{url} REGEXP url--
/select
注释的内容是开启正则匹配的,就是通过正则去匹配url,这里使用 url is not null and url != '' and #{url} REGEXP url 代替 url = #{url} ,然后在配置每个权限点时使用正则去配置每个权限点的url就可以了,比如刚才url配置权限校验时可以配置成/a/[5|6].json。当然这种方式对权限管理员的正则表达式有一定的要求。这时,在取符合条件的url时校验不过的权限点就取不出来了。取不出来不能直接就当做有处理,可以考虑遇到这种的再配置一个通配(/a/*.json)的权限,每次匹配到这种通配的url时必须保证匹配一个包含正则的校验才算有权限就可以了。这个的细节可以自己做一些不同的处理,这里只提供一个大概的思路。
结尾
关于数据权限就先说到这里,个人认为上面的方式在扩展性方面会相对好一些,实现起来难度也不是很大,如有问题欢迎指出。