对于类似需要设置用户只能查看哪些部门或层级的数据,这种情况一般称为数据权限。
例如对于销售,财务的数据,它们是非常敏感的,因此要求对数据权限进行控制, 对于基于集团性的应用系统而言,就更多需要控制好各自公司的数据了。如设置只能看本公司、或者本部门的数据,对于特殊的领导,可能需要跨部门的数据, 因此程序不能硬编码那个领导该访问哪些数据,需要进行后台的权限和数据权限的控制。
在以往的多账号系统开发过程中,对于不同登录用户层级权限并没有做过多的设计方案考虑,基本上都是简单粗暴的使用硬编码的方式,在特定的需要执行权限划分的方法中添加各种数据权限判断。
对于上面的场景,我们可以整理出如下固定的结构行为
【1】获取当前登录用户以及所关联的部门/层级权限标识
【2】拦截执行SQL,并添加对应的SQL扩展条件
针对上面所述的方式,一般可以通过两个实现方式去解决
【1】通过AOP的方式,拦截Service层方法,并在执行前添加SQL脚本,SQL脚本以${}变量的形式进行注入,注入的SQL可以是简单的where条件,比如dept_id=XX,又或者比较复杂的left join等等。这里需要注意因为是注入SQL,需要做好参数安全策略处理,避免SQL注入安全风险。
【2】通过ORM框架拦截器,比如MyBatis的拦截器,我们可以通过拦截器获取将要执行的SQL,然后在当前SQL外嵌套一个类似select * from (原来SQL) where dept_id=XX的方式,添加额外的数据权限筛选条件。
以下实现方式基于开源若依框架中关于数据权限设计,特整理记录:
【1】定义注解@DataScope,代码如下,其中定义了两个数据权限维度,一个是部门级别,一个是人员级别。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope
{
/**
* 部门表的别名
*/
public String deptAlias() default "";
/**
* 用户表的别名
*/
public String userAlias() default "";
}

部门数据权限注解

@DataScope(deptAlias = “d”)

@DataScope(deptAlias = “d”, userAlias = “u”)
逻辑实现代码 com.ruoyi.framework.aspectj.DataScopeAspect

上面代码中对将要执行的SQL拼接了额外的筛选条件,从而完成数据权限的筛选。
详细代码:
@Aspect
@Component
public class DataScopeAspect {
/**
* 全部数据权限
*/
public static final String DATA_SCOPE_ALL = "1";
/**
* 自定数据权限
*/
public static final String DATA_SCOPE_CUSTOM = "2";
/**
* 部门数据权限
*/
public static final String DATA_SCOPE_DEPT = "3";
/**
* 部门及以下数据权限
*/
public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
/**
* 仅本人数据权限
*/
public static final String DATA_SCOPE_SELF = "5";
/**
* 数据权限过滤关键字
*/
public static final String DATA_SCOPE = "dataScope";
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable {
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope) {
// 获取当前的用户
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNotNull(loginUser)) {
SysUser currentUser = loginUser.getUser();
// 如果是超级管理员,则不过滤数据
if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin()) {
dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
controllerDataScope.userAlias());
}
}
}
/**
* 数据范围过滤
*
* @param joinPoint 切点
* @param user 用户
* @param userAlias 别名
*/
public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias) {
StringBuilder sqlString = new StringBuilder();
for (SysRole role : user.getRoles()) {
String dataScope = role.getDataScope();
if (DATA_SCOPE_ALL.equals(dataScope)) {
sqlString = new StringBuilder();
break;
} else if (DATA_SCOPE_CUSTOM.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
role.getRoleId()));
} else if (DATA_SCOPE_DEPT.equals(dataScope)) {
sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
} else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope)) {
sqlString.append(StringUtils.format(
" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
deptAlias, user.getDeptId(), user.getDeptId()));
} else if (DATA_SCOPE_SELF.equals(dataScope)) {
if (StringUtils.isNotBlank(userAlias)) {
sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
} else {
// 数据权限为仅本人且没有userAlias别名不查询任何数据
sqlString.append(" OR 1=0 ");
}
}
}
if (StringUtils.isNotBlank(sqlString.toString())) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
}
}
}
/**
* 拼接权限sql前先清空params.dataScope参数防止注入
*/
private void clearDataScope(final JoinPoint joinPoint) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, "");
}
}
}
在mybatis查询底部标签添加数据范围过滤

例如:用户管理(未过滤数据权限的情况):

例如:用户管理(已过滤数据权限的情况):

结果很明显,我们多了如下语句。通过角色部门表(sys_role_dept)完成了数据权限过滤

【1】如果所示,通过注解和SQL注入变量的方式去处理数据权限,相对于硬编码的方式这样的处理更加简单。
【2】因为筛选条件是通过${}变量注入的方式,所以在当前切面需要进行适当的安全处理,避免出现SQL注入的危险。
【3】这种的方式需要配合sys_dept这类的单位/部门表进行关联,可能无法适配其他场景,需要进行适当的调整。
【4】当前方式是在ORM框架执行前做切面处理的,除此之外,我们也可以通过类似MyBatis的拦截器机拦截执行SQL进行场景处理。