文章目录
前言
由于若依的权限隔离过于简单,满足不了公司现有的需求,所以作者对现有的需求进行更改,这里作者采用了Groovy脚本对方法进行动态设置,然后传入当前用户信息和所有用户部门的信息,在脚本中进行计算并采用与若依一样的设计通过切面的方式将sql注入到目标sql中,这里将实现方式进行记录
一、前置条件
若依框架以及相关环境,Groovy 4.0.9
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.9</version>
</dependency>
二、使用步骤
1.配置以及注解扫描
首先加入
配置类在项目启动时对@DataScopePlus进行扫描
@Configuration
public class DataScopeConfig implements ApplicationContextAware, ApplicationRunner {
private ApplicationContext context;
/**
* 在新项目启动时获取被标注的@DataScopePlus标注的方法,如何将方法缓存以供用户设置
*/
public void doHandleDataScopeClass() {
Map<String,String> container = MapUtil.newHashMap();
Map<String, Object> serviceMap = context.getBeansWithAnnotation(Service.class);
Collection<Object> values = serviceMap.values();
for (Object value : values) {
if (AopUtils.isCglibProxy(value)) {
Class<?> clazz = AopUtils.getTargetClass(value);
for (Method method : clazz.getMethods()) {
if (method.isAnnotationPresent(DataScopePlus.class)) {
String clazzName = clazz.getName();
String methodName = method.getName();
container.put(clazzName, methodName);
}
}
}
}
//放入缓存或者放入数据库进行持久化,以供配置选择
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
@Override
public void run(ApplicationArguments args) throws Exception {
this.doHandleDataScopeClass();
}
}
/**
* 数据权限过滤注解
*
* @author 玄夜喀拉
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScopePlus {
}
2.实体设计与缓存
将被标记@DataScope的方法进行缓存,这里是实体Model信息
@Data
public class DataScopeCacheModel implements Serializable {
private static final long serialVersionUID=1L;
/**
* groovy脚本的id
*/
private String scriptId;
/**
* 特殊权限(拥于特殊权限的角色不进行权限隔离)
*/
private Set<String> spelRole;
/**
* 类名
*/
private String clazz;
/**
* 方法名
*/
private String method;
/**
* 表信息
*/
private Queue<DataScopeTableInfo> info;
/**
* 权限隔离表信息类
*/
static class DataScopeTableInfo{
/**
* 表别名
*/
private String tableAlias;
/**
* 字段别名
*/
private String fieldAlias;
/**
* 连接符 and 或者 or... 使用or可能会导致索引失效,这里要结合实际业务来实现
*/
private String connector;
}
}
3.切面的改造
这里对原有的若依切面进行了改造,因为若依原有的数据隔离方式过少,不符合公司现有的需求,这里改为了通过脚本的方式调用,如何发生异常了为了防止数据泄露直接拼接1!=1进行数据的隔离
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScopePlus controllerDataScope) {
try {
clearDataScope(point);
Signature signature = point.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
//通过className和MethodName获取缓存的信息
//....假设这里已经获取到了
DataScopeCacheModel cache=...;
if (ObjectUtil.isEmpty(cache)) {
this.setSqlString(point, "1!=1");
return;
}
String scriptId = cache.getScriptId();
//通过缓存的脚本获取脚本内容
String content="....."
if (ObjectUtil.isEmpty(content)) {
this.setSqlString(point, "1!=1");
return;
}
handleDataScope(point, cache, content);
} catch (Exception e) {
log.error("数据权限判断错误:{},异常信息:", SecurityUtils.getUserName(), e);
this.setSqlString(point, "1!=1");
}
}
/**
* 处理数据权限信息
*
* @param joinPoint 切点
* @param cache 缓存
* @param content 脚本内容
*/
protected void handleDataScope(final JoinPoint joinPoint, DataScopeCacheModel cache, String content) {
// 获取当前的用户
//如果是vip,跳过鉴权,这里其实就是当前用户的权限id与目标的权限id取交集
if (checkSpel(cache.getSpelRole())) {
return;
}
//执行脚本并返回拼接的sql
String sqlString = this.executeScript(content, cache);
this.setSqlString(joinPoint, sqlString);
}
/**
* 执行脚本
*
* @param script 脚本内容
* @return 返回拼接的sql
*/
private String executeScript(String script, DataScopeCacheModel cache) {
Binding binding = new Binding();
binding.setVariable(USER, SecurityUtils.getUserId());
binding.setVariable(DEPT, SecurityUtils.getDeptId());
//...略,这里把用户信息与接口的信息传入Groovy脚本
GroovyShell shell = new GroovyShell(binding);
return (String) shell.evaluate(script);
}
4.Groovy脚本的实现
这里仅对一个字段进行了实现,如何存在多个字段进行sql的拼接的情况,这里的sqlString两个字段会相同,这里可以加入具体的权限进行实现,譬如说当前用户,当前用户的部分用户,当前用户的下级等条件。
这里的脚本仅针对了一个字段,如果有多个字段的话,我能想到的就是继续添加脚本,因为字段的数量是可控的但是与字段拼接的值还需要和业务相匹配,如果随着业务的增加,这里的脚本可能会越来越多
/**
* 自己只能看自己的,部门领导可以看部门下的所有数据
* @author 玄夜喀拉
*/
def userId = (String) binding.getVariable(DataScopePlusAspect.USER)
def deptId = (String) binding.getVariable(DataScopePlusAspect.DEPT)
def tableAlias = (String) binding.getVariable(DataScopePlusAspect.TABLE_ALIAS)
def fieldAlias = (String) binding.getVariable(DataScopePlusAspect.FIELD_ALIAS)
def andAdd = (Boolean) binding.getVariable(DataScopePlusAspect.AND_ADD)
//部门表
def deptList = ...这里获取部门的列表
//用户表
def userList = ...这里获取用户的列表
//构建部门map和用户map
def deptMap = deptList.stream().collect(Collectors.toMap((SysDept k) -> String.valueOf(k.getDeptId()), Function.identity()))
//获取部门与用户list的映射
def deptUsersMap = userList.stream().collect(Collectors.groupingBy({ SysUser user -> String.valueOf(user.getDeptId()) }, Collectors.mapping({ SysUser user -> String.valueOf(user.getUserId()) }, Collectors.toList())))
//最终可查看的用户数据
Set<String> result = new HashSet<>()
//获取用户的子部门信息
//找出该用户所在部门的子部门
List<String> childDeptIds = new ArrayList<>();
for (SysDept deptItem : deptList) {
String ancestors = deptItem.getAncestors();
if (ancestors.contains(String.valueOf(deptId))) {
childDeptIds.add(String.valueOf(deptItem.getDeptId()));
}
}
//判断,如果是叶子节点,则只查看该用户自己的数据
if (childDeptIds.size() == 0) {
//判断该部门的领导是不是当前用户
def sysDept = deptMap.get(deptId)
//是否为该部门领导,找出该部门的用户
if (userId == sysDept.leaderId.toString()) { //是领导
def userIds = deptUsersMap.get(deptId);
if (userIds) {
//获取该部门的所有用户
result.addAll(userIds)
}
} else {
//不是领导添加自己即可
result.add(userId)
}
} else {//有子部门,说明该用户不为普通人,直接找他的部门和子部门的所有用户
//将自己的部门也加进去
childDeptIds.add(deptId)
childDeptIds.each({ child ->
{
List<String> userIds = deptUsersMap.get(child)
if (userIds) {
result.addAll(userIds)
}
}
})
}
//遍历部门表,查看leader是否相等
deptList.each({ deptInfo ->
if (String.valueOf(deptInfo.leaderId) == userId && String.valueOf(deptInfo.deptId) != deptId) {
childDeptIds.add(String.valueOf(deptInfo.deptId))
childDeptIds.each({ item ->
List<String> userIds = deptUsersMap.get(item)
if (userIds) {
result.addAll(userIds)
}
})
}
})
//拼接用户字符串
def sqlString = StrUtil.join(",", result)
String table = ""
if ("" != tableAlias) {
table = "${tableAlias}."
}
if ("" == fieldAlias) {
fieldAlias = "create_by"
}
return "${andAdd ? 'and' : ''} ${table} ${fieldAlias} in (${sqlString})".toString()
总结
Groovy是一个基于JVM的脚本语言,它可以与Java互相调用,相较于原有的方式,使用groovy的方式程序效率相较于若依原版会更加低一点,而且依赖Groovy环境容易出现问题,但是这种实现方式更加灵活可控,能完成一些复杂逻辑的代码。