上周在公司,一起跟mentor做了一些平台权限控制的工作。业务逻辑非常复杂,花了大量的时间去理解这个平台权限管理的设计。
首先,它的权限管理系统并不是我们自己部门开发的,是用的另外一套系统,在这一套系统上面,去自定义配置权限,可以配置的自由度非常大,甚至每一个按钮,每一条数据都可以定义它的权限范围。但这同时也意味着,高自由度的同时性能又很难去保证。并且我觉得反人类的是,在这个平台上面可以给用户配置角色,一套权限对应一套权限,这很正常对吧,但是这套权限,里面配置的不是这个角色有哪些权限,而是配置这些权限,是个人权限还是部门权限,部门权限的话要配置有权限的部门,可以多个部门都有权限。听起来很绕,但我也不知道怎么表述它,就是很反直觉,我也不明白这样设计的好处是什么,也许可能这样方便跨部门授权吧。然后用户所在的部门,跟权限平台配置的部门一样,或者是权限平台配置的部门下的所有子孙部门,那这个用户就是有权限访问这个资源的。
我要做的工作,就是在我们的平台登录之后进行加载的时候,会请求一个接口,这个接口里面去远程调用权限平台的当前用户的所有权限信息。权限平台返回的权限信息是一串非常长的JSON串,因为每一层菜单都配置了权限,菜单里面又有子菜单,所以我要做的就是,去递归的过滤这些JSON里的资源,只留下当前用户有权限的资源。每一层菜单,它其实是一个map,里面有一个department数组,就是看当前用户所在部门属不属于department数组中的部门和它们的子孙部门,如果不是,那就去掉,它的下一级子菜单也就没必要继续递归过滤了,如果是,那就保留,如果有子菜单继续递归过滤下一级子菜单。逻辑就是这样的一个逻辑,但是我把它刚写出来的时候,效率非常非常非常慢,因为菜单个数很多,每一个菜单遇到department都要去查询department的子孙部门,看有没有匹配上的,查询department的子孙部门的接口还不是一个本地的方法,还要去远程调用另外一个系统中的接口,所以一次递归过滤可能会发送上百个远程调用,效率之底可见,一个请求可能要花上几十秒。于是我就开始了优化之路,先是检查递归有没有能够剪枝的地方,尽量保证递归不做一些无用功,然后我再把查询部门的子孙部门的接口写到本地,并且做一个redis缓存,下次查到相同的部门的子孙部门可以直接从缓存中拿。这样优化下来,效率大大提升了,接口从几十秒优化到了三秒内,但是对于一般的接口,三秒钟的返回时间还是难以忍受的,我还需要继续优化,于是我发现其实在一次过滤的过程中,它会请求上百次部门的子部门接口,但其实大部门查询的部门id是相同的,因为配置权限的人会把很多菜单的权限配置成相同的部门,虽然我已经用上了redis缓存,但上百次请求的,还是需要和redis有上百次IO,我们知道性能瓶颈大头还是在于IO,于是我想在递归内部加一个缓存,由于用户自己的部门是不变的,如果和要求的部门匹配上,那下一次遇到同样的部门,匹配的时候还是匹配的上的,不用再去查接口查redis,并且这个缓存要在整个递归的生命周期都生效。我于是就给递归的传参加上了两个Set, authorizedSet和unAuthorizedSet分别代表递归中查询过有权限和没权限的部门集合,避免重复调用查询,如下:
private List<Map<String, Object>> loopFilterResource(List<Map<String, Object>> resourceList,
List<Map<String, Object>> filteredList,
long departmentId,
String tenant,
Set<Long> authorizedSet,
Set<Long> unAuthorizedSet) {
if (CollectionUtils.isEmpty(resourceList)) {
return null;
}
for (Map map : resourceList) {
boolean isAuthorized = false;
if ((Integer)map.get(Constants.PERMISSION_LEVEL) == 0) {
// 如果是个人权限
isAuthorized = true;
} else {
JSONArray requiredParentDepartmentIds = (JSONArray)map.get(Constants.DEPARTMENT_IDS);
for (int i = 0; i < requiredParentDepartmentIds.size(); i++) {
long requiredParentDepartmentId = requiredParentDepartmentIds.getLong(i);
if (authorizedSet.contains(requiredParentDepartmentId)) {
isAuthorized = true;
break;
}
if (unAuthorizedSet.contains(requiredParentDepartmentId)) {
continue;
}
// 获取这个部门的下属部门与它自身的集合
Set<Long> parentsAndChildrenDepartmentId = getAllChildrenDepartments(requiredParentDepartmentId, tenant);
// 如果包含用户所在部门,则把isAuthorized设置为true
if (parentsAndChildrenDepartmentId.contains(departmentId)) {
// authorizedSet代表在递归中查询过的部门,且有权限的部门
authorizedSet.add(requiredParentDepartmentId);
isAuthorized = true;
break;
} else {
// unAuthorizedSet代表在递归中查询过的部门,但用户没有权限的部门
unAuthorizedSet.add(requiredParentDepartmentId);
}
}
}
if (isAuthorized) {
filteredList.add(map);
List children = (List)map.get(Constants.CHILDREN);
List filteredChildren = loopFilterResource(children, new ArrayList<>(), departmentId, tenant, authorizedSet, unAuthorizedSet);
filteredList.get(filteredList.size() - 1).put(Constants.CHILDREN, filteredChildren);
}
}
return filteredList;
}
这样之后,IO的次数就大大减少了,一个部门的id只会去查询一次,只发生一次IO,效率大大提高,接口返回时间也从3秒钟降低到了300ms左右。如果再对过滤完的资源进行缓存话,之后查看权限都走的是缓存,这样系统的压力也大大减少了。
除了递归过滤有权限的资源,还要对一些按钮做部门级的权限校验,并且进行一个缓存模式的改造,但也不算特别难实现,就不在此多说了。