一、背景与现状问题
在日常业务系统中,采购、订单、合同等核心数据往往按部门、按用户严格隔离。随着业务扩张,管理者(上级)需要查看下级人员负责的数据,进行监督、审批与统计,由此产生两类突出痛点:
一是数据权限过滤逻辑散落各处。各查询接口需要手工拼接部门、用户等过滤条件,代码重复、口径不一,极易遗漏或写错,存在越权访问与数据泄露风险。
二是上级查看下级数据困难。多级部门体系下,需要按部门层级做前缀匹配才能覆盖下属范围,手工实现繁琐且容易遗漏层级,导致上级无法完整看到下属数据,影响管理与决策。
三是合规管控薄弱。缺乏统一的豁免与鉴权机制,特殊角色(如超级管理员、供应商管理员)与普通用户的过滤规则混在一起,权限边界不清晰。
二、改进方案
(一)总体思路
设计行级数据权限控制注解@DataPermission,标注该注解的查询方法,在执行时由切面自动根据当前登录用户的属性拼接数据过滤条件,达到按用户、部门隔离数据的目的,核心解决上级查看下级数据的问题。
(二)技术方案设计
组件定位为行级数据权限控制注解。生效方式采用AOP切面加ThreadLocal加MyBatis拦截器的组合:AOP切面负责拦截带注解的方法并获取当前登录用户信息存入ThreadLocal;MyBatis拦截器在执行查询时读取ThreadLocal中的用户上下文,自动为SQL拼接数据权限过滤条件。
组件通过fields配置实现字段与取值来源的映射,关键配置项说明如下表:
表1 注解配置项说明

(三)关键实现点
一是AOP切面拦截:标注@DataPermission的方法被调用时,切面获取当前登录用户信息,写入ThreadLocal,供后续MyBatis拦截器使用,保证线程内上下文正确隔离。
二是MyBatis拦截器拼装过滤条件:拦截查询SQL,读取ThreadLocal中的用户上下文,根据fields配置的字段映射,将用户属性值作为参数拼入WHERE条件,实现自动过滤。
三是多级部门前缀匹配:启用defaultLevelCode时,按当前用户的deptLevelCode做like前缀匹配,自动覆盖其下各级部门的全部数据,从而让上级完整查看下属数据。
四是角色豁免机制:命中notRoleId配置的角色时不加过滤条件,例如超级管理员可查看全量数据、供应商管理员只管理本供应商数据,实现权限边界清晰可控。
(四)使用示例
在需要数据权限控制的查询方法上标注注解,并配置对应的字段映射即可,例如:@DataPermission(fields = {"dept_id = deptLevelCode"}, defaultLevelCode = true)。方法执行时即自动完成数据隔离,业务代码无需再手工编写过滤逻辑。
三、预期收益
(一)质量收益
统一数据权限过滤口径,消除散落重复的过滤代码,降低越权访问与数据泄露风险,数据隔离更严谨、更可靠。
(二)效率收益
上级无需逐级手工查询即可完整查看下属数据,管理、审批与统计效率显著提升;开发人员新增查询接口时只需加一个注解,无需重复编写过滤逻辑,开发效率大幅提高。
(三)合规收益
通过统一的角色豁免机制与权限边界管控,实现按角色、按部门、按用户的精细化授权,满足数据合规与审计要求。
五、适用范围与限制
一是该组件适用于需要按用户、部门隔离数据的查询场景,如采购、订单、合同、台账等业务;
二是依赖当前登录用户上下文,接口调用链中必须能获取到CommonUserVO用户信息;
三是fields的key需与目标SQL或实体字段名保持一致,value需为CommonUserVO中存在的属性名,否则过滤条件无法正确拼装;
四是非查询类操作(如批量写入)不适用,需结合业务另行处理;
五是特殊角色的豁免配置需与业务权限体系保持一致,避免误配导致数据越权。
四 、相关代码
(1)DataPermission 注解
java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
/**
* 使用 KeyValue 数组来表示多个字段和对应的值
*/
KeyValue[] fields() default {};
/**
* 是否使用levelCode控权
*/
boolean defaultLevelCode() default false;
}
(2)DataPermissionAspect 切面
java
package com.mdgyl.data.mybatis.plus.anno;
import com.jxdinfo.hussar.common.security.BaseSecurityUtil;
import com.jxdinfo.hussar.common.security.SecurityUser;
import com.mdgyl.common.cache.redis.util.RedisUtils;
import com.mdgyl.common.constants.RedisConstants;
import com.mdgyl.common.helper.CollectionHelper;
import com.mdgyl.common.vo.CommonUserVO;
import com.mdgyl.data.mybatis.plus.permission.DataPermissionContext;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class DataPermissionAspect {
@Value("${permission.levelCode.roleId}")
private String levelCodeRoleId;
@Value("${permission.not.roleId}")
private String notRoleId;
@Resource
private RedisUtils redisUtils;
@Around("@annotation(com.mdgyl.data.mybatis.plus.anno.DataPermission)")
public Object addDataPermission(ProceedingJoinPoint joinPoint) throws Throwable {
final Signature signature = joinPoint.getSignature();
final Object[] args = joinPoint.getArgs();
if (!(signature instanceof MethodSignature)) {
return joinPoint.proceed(args);
}
final Method method = ((MethodSignature) signature).getMethod();
DataPermission dataPermission = method.getAnnotation(DataPermission.class);
SecurityUser securityUser = BaseSecurityUtil.getUser();
Long userId = securityUser == null ? null : securityUser.getUserId();
List<Long> rolesList = securityUser == null ? null : securityUser.getRolesList();
boolean queryPermission = containsNotRoleId(rolesList, notRoleId);
if (queryPermission) {
log.info("user is supplier manager or super admin id is {}", userId);
return joinPoint.proceed(args);
}
Object o = redisUtils.hget(RedisConstants.PURCHASE_CENTER_USER_HASH_KEY, String.valueOf(userId));
if (o == null) {
log.error("DataPermissionAspect query user redis is null userId is {}", userId);
return joinPoint.proceed(args);
}
CommonUserVO commonUserVO = (CommonUserVO) o;
// 获取 @DataPermission 注解中的字段和值
KeyValue[] keyValues = dataPermission.fields();
boolean defaultLevelCode = dataPermission.defaultLevelCode();
boolean permissionDeptLevelCode = isPermissionDeptLevelCode(rolesList,defaultLevelCode);
// 动态添加数据权限过滤条件
Map<String, Object> fieldMapping = new HashMap<>();
for (KeyValue keyValue : keyValues) {
String fieldName = keyValue.key();
String value = keyValue.value();
Object propertyValue = getPropertyValue(commonUserVO, value);
if (CommonUserVO.PERMISSION_DEPT_LEVEL_CODE.equals(value)) {
if(permissionDeptLevelCode){
fieldMapping.put(fieldName + ";like", propertyValue);
}
} else {
fieldMapping.put(fieldName + ";=", propertyValue);
}
}
try {
// 将用户信息绑定到当前线程上下文
DataPermissionContext.set(userId, fieldMapping);
// 执行目标方法
return joinPoint.proceed();
} finally {
// 清理上下文,避免线程污染
DataPermissionContext.clear();
}
}
public static boolean containsNotRoleId(List<Long> rolesList, String roleIdStr) {
boolean permission = false;
if (StringUtils.isBlank(roleIdStr)) {
return false;
}
String[] roleIds = roleIdStr.split(",");
for (String roleId : roleIds) {
if (CollectionUtils.isEmpty(rolesList)) {
break;
}
if (rolesList.contains(Long.parseLong(roleId))) {
permission = true;
}
}
return permission;
}
private boolean isPermissionDeptLevelCode(List<Long> rolesList, boolean defaultLevelCode) {
// 判断是否要增加 部门编码的权限条件
boolean levelCode = false;
// 判断是否默认允许levelCode控权
if(defaultLevelCode){
return true;
}
if (StringUtils.isBlank(levelCodeRoleId)) {
return false;
}
String[] roleIds = levelCodeRoleId.split(",");
for (String roleId : roleIds) {
if (CollectionUtils.isEmpty(rolesList)) {
break;
}
if (rolesList.contains(Long.parseLong(roleId))) {
levelCode = true;
}
}
return levelCode;
}
/**
* 根据属性名获取对象的属性值
*
* @param object 目标对象
* @param fieldName 属性名
* @return 属性值
* @throws NoSuchFieldException 如果找不到属性
* @throws IllegalAccessException 如果无法访问属性
*/
public static Object getPropertyValue(Object object, String fieldName) throws NoSuchFieldException, IllegalAccessException {
// 获取对象的 Class 类型
Class<?> clazz = object.getClass();
// 获取目标属性的 Field 对象
Field field = clazz.getDeclaredField(fieldName);
// 设置属性可访问(即使它是私有的)
field.setAccessible(true);
// 返回字段值
return field.get(object);
}
}
(3)KeyValue 注解
java
package com.mdgyl.data.mybatis.plus.anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface KeyValue {
String key();
String value();
}
(4)用法
java
@DataPermission(fields = {
//计划岗控权
@KeyValue(key = MaterialDO.PERMISSION_PLAN_USER_ID, value = CommonUserVO.PERMISSION_USER_ID),
@KeyValue(key = MaterialDO.PERMISSION_PLAN_USER_SUPERIOR_ID, value = CommonUserVO.PERMISSION_USER_ID),
@KeyValue(key = MaterialDO.PERMISSION_PLAN_USER_DEPT_LEVEL_CODE, value = CommonUserVO.PERMISSION_DEPT_LEVEL_CODE),
//人员岗控权
@KeyValue(key = MaterialDO.PERMISSION_PRICE_USER_ID, value = CommonUserVO.PERMISSION_USER_ID),
@KeyValue(key = MaterialDO.PERMISSION_PRICE_USER_SUPERIOR_ID, value = CommonUserVO.PERMISSION_USER_ID),
@KeyValue(key = MaterialDO.PERMISSION_PRICE_USER_DEPT_LEVEL_CODE, value = CommonUserVO.PERMISSION_DEPT_LEVEL_CODE),
})
public List<MaterialOrgDO> listByPermissionPageParam(MaterialPageQueryParam pageQueryParam) {
LambdaQueryWrapper<MaterialOrgDO> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MaterialOrgDO::getTenantId, pageQueryParam.getTenantId());
return materialOrgMapper.selectList(queryWrapper);
}
java
@Setter
@Getter
@ToString
@TableName(value = "t_material", autoResultMap = true)
public class MaterialDO implements BaseEntity {
@TableField(exist = false)//计划岗人员id控权
public static final String PERMISSION_PLAN_USER_ID = "plan_user_id";
@TableField(exist = false)//计划岗人员部门编码控权
public static final String PERMISSION_PLAN_USER_DEPT_LEVEL_CODE = "plan_user_dept_level_code";
@TableField(exist = false)//计划岗人员上级用户id控权
public static final String PERMISSION_PLAN_USER_SUPERIOR_ID = "plan_user_superior_id";
@TableField(exist = false)
public static final String PERMISSION_PRICE_USER_ID = "price_user_id";
@TableField(exist = false)
public static final String PERMISSION_PRICE_USER_DEPT_LEVEL_CODE = "price_user_dept_level_code";
@TableField(exist = false)
public static final String PERMISSION_PRICE_USER_SUPERIOR_ID = "price_user_superior_id";
@TableField(exist=false)
public static final String PERMISSION_DEVELOP_USER_ID="develop_user_id";