数据权限控制实现方案:让数据访问井井有条!🔒

57 阅读8分钟

标题: 数据权限还在代码里写死?动态权限来了!
副标题: 从行级过滤到部门隔离,数据安全全方位防护


🎬 开篇:一次数据泄露的严重事故

某企业CRM系统:

销售A:登录系统,查看客户列表
系统:返回全公司10万客户信息 💀

销售A:离职带走客户资料
公司:客户流失,损失500万 💸

原因分析:
- 没有数据权限控制
- 所有销售能看到所有客户
- 没有部门隔离
- 没有操作审计

改造后(数据权限控制):
销售A:登录系统
系统:只返回A所在部门的客户 ✅

效果:
- 数据按部门隔离
- 只能看到自己负责的客户
- 操作全程审计
- 离职带不走数据

老板:这才对嘛! 😊

教训:数据权限控制是企业数据安全的核心!

🤔 什么是数据权限控制?

想象一下:

  • 部门经理: 只能看本部门的数据
  • 区域经理: 只能看本区域的数据
  • 销售人员: 只能看自己负责的客户
  • 财务人员: 能看所有数据但不能修改

数据权限控制 = 行级过滤 + 字段级控制 + 动态SQL拼接!


📚 知识地图

数据权限控制
├── 🎯 权限类型
│   ├── 部门权限(本部门、本部门及下级)
│   ├── 个人权限(仅本人)
│   ├── 角色权限(按角色划分)
│   ├── 自定义权限(动态配置)
│   └── 全部权限(管理员)
├── 🏗️ 实现方式
│   ├── SQL拦截(MyBatis插件)⭐⭐⭐⭐⭐
│   ├── AOP切面(方法拦截)⭐⭐⭐⭐
│   ├── 手动拼接(硬编码)⭐⭐
│   └── 视图过滤(数据库视图)⭐⭐⭐
├── ⚡ 核心功能
│   ├── 行级过滤
│   ├── 字段级控制
│   ├── 操作权限
│   └── 数据范围
└── 🛡️ 安全防护
    ├── 权限缓存
    ├── 操作审计
    ├── 数据脱敏
    └── 异常告警

💾 数据库设计

-- 数据权限配置表
CREATE TABLE data_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    role_id BIGINT NOT NULL COMMENT '角色ID',
    permission_type TINYINT NOT NULL COMMENT '权限类型:1全部 2本部门 3本部门及下级 4仅本人 5自定义',
    dept_ids VARCHAR(500) COMMENT '部门ID列表(逗号分隔)',
    user_ids VARCHAR(500) COMMENT '用户ID列表(逗号分隔)',
    custom_sql VARCHAR(1000) COMMENT '自定义SQL条件',
    resource_type VARCHAR(50) NOT NULL COMMENT '资源类型:customer/order/contract',
    enable_field_control TINYINT NOT NULL DEFAULT 0 COMMENT '是否启用字段级控制',
    visible_fields VARCHAR(500) COMMENT '可见字段列表',
    editable_fields VARCHAR(500) COMMENT '可编辑字段列表',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_role_resource (role_id, resource_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据权限配置表';

-- 用户部门关系表
CREATE TABLE user_dept (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL COMMENT '用户ID',
    dept_id BIGINT NOT NULL COMMENT '部门ID',
    is_leader TINYINT NOT NULL DEFAULT 0 COMMENT '是否部门负责人',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_user_dept (user_id, dept_id),
    INDEX idx_dept_id (dept_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户部门关系表';

-- 部门表
CREATE TABLE department (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父部门ID',
    name VARCHAR(100) NOT NULL COMMENT '部门名称',
    level INT NOT NULL DEFAULT 1 COMMENT '层级',
    path VARCHAR(500) NOT NULL COMMENT '路径:1/2/3',
    leader_id BIGINT COMMENT '部门负责人ID',
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_parent_id (parent_id),
    INDEX idx_path (path)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门表';

🔧 方案1:MyBatis拦截器(推荐)

自定义注解

/**
 * 数据权限注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataPermission {
    
    /**
     * 数据权限类型
     */
    PermissionType type() default PermissionType.DEPT;
    
    /**
     * 用户ID字段名
     */
    String userIdColumn() default "user_id";
    
    /**
     * 部门ID字段名
     */
    String deptIdColumn() default "dept_id";
    
    /**
     * 表别名
     */
    String tableAlias() default "";
}

/**
 * 权限类型
 */
public enum PermissionType {
    
    /**
     * 全部数据
     */
    ALL,
    
    /**
     * 本部门数据
     */
    DEPT,
    
    /**
     * 本部门及下级部门数据
     */
    DEPT_AND_CHILD,
    
    /**
     * 仅本人数据
     */
    SELF,
    
    /**
     * 自定义
     */
    CUSTOM
}

MyBatis拦截器

/**
 * 数据权限拦截器
 */
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
@Component
@Slf4j
public class DataPermissionInterceptor implements Interceptor {
    
    @Autowired
    private DataPermissionService dataPermissionService;
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        
        // 获取MappedStatement
        MappedStatement mappedStatement = (MappedStatement) 
            metaObject.getValue("delegate.mappedStatement");
        
        // 只拦截SELECT语句
        if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()) {
            return invocation.proceed();
        }
        
        // 获取方法注解
        DataPermission dataPermission = getDataPermission(mappedStatement);
        
        if (dataPermission == null) {
            // 没有注解,不拦截
            return invocation.proceed();
        }
        
        // 获取原始SQL
        BoundSql boundSql = statementHandler.getBoundSql();
        String originalSql = boundSql.getSql();
        
        log.debug("原始SQL:{}", originalSql);
        
        // ⚡ 构建数据权限SQL
        String permissionSql = buildPermissionSql(originalSql, dataPermission);
        
        log.debug("权限SQL:{}", permissionSql);
        
        // 替换SQL
        metaObject.setValue("delegate.boundSql.sql", permissionSql);
        
        return invocation.proceed();
    }
    
    /**
     * 获取DataPermission注解
     */
    private DataPermission getDataPermission(MappedStatement mappedStatement) {
        try {
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            String methodName = id.substring(id.lastIndexOf(".") + 1);
            
            Class<?> clazz = Class.forName(className);
            
            // 先查找方法上的注解
            for (Method method : clazz.getDeclaredMethods()) {
                if (method.getName().equals(methodName)) {
                    DataPermission annotation = method.getAnnotation(DataPermission.class);
                    if (annotation != null) {
                        return annotation;
                    }
                }
            }
            
            // 再查找类上的注解
            return clazz.getAnnotation(DataPermission.class);
            
        } catch (Exception e) {
            log.error("获取DataPermission注解失败", e);
            return null;
        }
    }
    
    /**
     * 构建数据权限SQL
     */
    private String buildPermissionSql(String originalSql, DataPermission dataPermission) {
        // 1. 获取当前用户
        UserContext currentUser = UserContextHolder.get();
        
        if (currentUser == null) {
            return originalSql;
        }
        
        // 2. 判断是否是管理员(管理员不限制)
        if (currentUser.isAdmin()) {
            return originalSql;
        }
        
        // 3. 根据权限类型构建SQL条件
        String condition = buildCondition(currentUser, dataPermission);
        
        if (StringUtils.isBlank(condition)) {
            return originalSql;
        }
        
        // 4. ⚡ 拼接SQL(在WHERE子句后添加条件)
        return injectCondition(originalSql, condition);
    }
    
    /**
     * 构建SQL条件
     */
    private String buildCondition(UserContext user, DataPermission dataPermission) {
        String tableAlias = StringUtils.isNotBlank(dataPermission.tableAlias()) 
            ? dataPermission.tableAlias() + "." : "";
        
        PermissionType type = dataPermission.type();
        
        switch (type) {
            case ALL:
                // 全部数据,不添加条件
                return "";
                
            case SELF:
                // 仅本人数据
                return String.format("%s%s = %d", 
                    tableAlias, dataPermission.userIdColumn(), user.getUserId());
                
            case DEPT:
                // 本部门数据
                return String.format("%s%s = %d", 
                    tableAlias, dataPermission.deptIdColumn(), user.getDeptId());
                
            case DEPT_AND_CHILD:
                // 本部门及下级部门数据
                List<Long> deptIds = dataPermissionService.getDeptAndChildIds(user.getDeptId());
                String deptIdsStr = deptIds.stream()
                    .map(String::valueOf)
                    .collect(Collectors.joining(","));
                
                return String.format("%s%s IN (%s)", 
                    tableAlias, dataPermission.deptIdColumn(), deptIdsStr);
                
            case CUSTOM:
                // 自定义条件
                return dataPermissionService.getCustomCondition(user.getRoleId());
                
            default:
                return "";
        }
    }
    
    /**
     * 注入SQL条件
     */
    private String injectCondition(String originalSql, String condition) {
        // 使用JSqlParser解析SQL
        try {
            Statement statement = CCJSqlParserUtil.parse(originalSql);
            
            if (statement instanceof Select) {
                Select select = (Select) statement;
                PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
                
                // 获取WHERE子句
                Expression where = plainSelect.getWhere();
                
                // 构建新的条件
                Expression newCondition = CCJSqlParserUtil.parseCondExpression(condition);
                
                if (where != null) {
                    // 已有WHERE条件,使用AND连接
                    AndExpression andExpression = new AndExpression(where, newCondition);
                    plainSelect.setWhere(andExpression);
                } else {
                    // 没有WHERE条件,直接设置
                    plainSelect.setWhere(newCondition);
                }
                
                return select.toString();
            }
            
        } catch (Exception e) {
            log.error("注入SQL条件失败:sql={}, condition={}", originalSql, condition, e);
        }
        
        return originalSql;
    }
    
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    
    @Override
    public void setProperties(Properties properties) {
        // 设置配置参数
    }
}

使用示例

/**
 * 客户Mapper
 */
@Mapper
public interface CustomerMapper extends BaseMapper<Customer> {
    
    /**
     * ⚡ 查询客户列表(自动添加数据权限)
     */
    @DataPermission(
        type = PermissionType.DEPT_AND_CHILD,
        deptIdColumn = "dept_id",
        tableAlias = "c"
    )
    @Select("SELECT * FROM customer c WHERE c.status = 1")
    List<Customer> selectList();
    
    /**
     * ⚡ 查询客户详情(自动添加数据权限)
     */
    @DataPermission(
        type = PermissionType.DEPT,
        deptIdColumn = "dept_id"
    )
    Customer selectById(@Param("id") Long id);
}

/**
 * Service示例
 */
@Service
public class CustomerService {
    
    @Autowired
    private CustomerMapper customerMapper;
    
    /**
     * 查询客户列表
     * 
     * 用户A(部门1)调用:
     * SQL:SELECT * FROM customer c WHERE c.status = 1 AND c.dept_id IN (1, 2, 3)
     * 
     * 用户B(部门2)调用:
     * SQL:SELECT * FROM customer c WHERE c.status = 1 AND c.dept_id IN (2, 4, 5)
     * 
     * ⚡ 不同用户自动过滤不同数据
     */
    public List<Customer> getCustomerList() {
        return customerMapper.selectList();
    }
}

🔧 方案2:AOP切面

/**
 * 数据权限切面
 */
@Aspect
@Component
@Slf4j
public class DataPermissionAspect {
    
    @Autowired
    private DataPermissionService dataPermissionService;
    
    /**
     * 环绕通知
     */
    @Around("@annotation(dataPermission)")
    public Object around(ProceedingJoinPoint pjp, DataPermission dataPermission) 
        throws Throwable {
        
        // 1. 获取当前用户
        UserContext currentUser = UserContextHolder.get();
        
        if (currentUser == null || currentUser.isAdmin()) {
            // 未登录或管理员,直接放行
            return pjp.proceed();
        }
        
        // 2. ⚡ 设置数据权限上下文
        DataPermissionContext context = new DataPermissionContext();
        context.setUserId(currentUser.getUserId());
        context.setDeptId(currentUser.getDeptId());
        context.setRoleId(currentUser.getRoleId());
        context.setPermissionType(dataPermission.type());
        
        DataPermissionContextHolder.set(context);
        
        try {
            // 3. 执行方法
            return pjp.proceed();
            
        } finally {
            // 4. 清除上下文
            DataPermissionContextHolder.clear();
        }
    }
}

/**
 * 数据权限上下文
 */
@Data
public class DataPermissionContext {
    
    private Long userId;
    private Long deptId;
    private Long roleId;
    private PermissionType permissionType;
    private List<Long> deptIds;
}

/**
 * 数据权限上下文持有者
 */
public class DataPermissionContextHolder {
    
    private static final ThreadLocal<DataPermissionContext> CONTEXT_HOLDER = 
        new ThreadLocal<>();
    
    public static void set(DataPermissionContext context) {
        CONTEXT_HOLDER.set(context);
    }
    
    public static DataPermissionContext get() {
        return CONTEXT_HOLDER.get();
    }
    
    public static void clear() {
        CONTEXT_HOLDER.remove();
    }
}

🎯 方案3:动态SQL拼接

/**
 * 数据权限服务
 */
@Service
public class DataPermissionService {
    
    @Autowired
    private DepartmentMapper departmentMapper;
    
    @Autowired
    private DataPermissionMapper dataPermissionMapper;
    
    /**
     * 获取部门及所有下级部门ID
     */
    public List<Long> getDeptAndChildIds(Long deptId) {
        // 查询部门信息
        Department dept = departmentMapper.selectById(deptId);
        
        if (dept == null) {
            return Collections.singletonList(deptId);
        }
        
        // ⚡ 使用path字段查询所有子部门
        List<Department> children = departmentMapper.selectByPathPrefix(dept.getPath() + "/");
        
        List<Long> deptIds = new ArrayList<>();
        deptIds.add(deptId);
        
        if (!children.isEmpty()) {
            deptIds.addAll(
                children.stream()
                    .map(Department::getId)
                    .collect(Collectors.toList())
            );
        }
        
        return deptIds;
    }
    
    /**
     * 获取自定义SQL条件
     */
    public String getCustomCondition(Long roleId) {
        DataPermission permission = dataPermissionMapper.selectByRoleId(roleId);
        
        if (permission != null && StringUtils.isNotBlank(permission.getCustomSql())) {
            return permission.getCustomSql();
        }
        
        return "";
    }
    
    /**
     * 检查字段访问权限
     */
    public boolean hasFieldPermission(Long roleId, String resourceType, String field) {
        DataPermission permission = dataPermissionMapper.selectByRoleAndResource(
            roleId, resourceType);
        
        if (permission == null || !permission.getEnableFieldControl()) {
            return true;  // 未配置字段权限,默认允许
        }
        
        String visibleFields = permission.getVisibleFields();
        
        if (StringUtils.isBlank(visibleFields)) {
            return true;
        }
        
        // 判断字段是否在可见列表中
        return Arrays.asList(visibleFields.split(",")).contains(field);
    }
    
    /**
     * 检查编辑权限
     */
    public boolean hasEditPermission(Long roleId, String resourceType, String field) {
        DataPermission permission = dataPermissionMapper.selectByRoleAndResource(
            roleId, resourceType);
        
        if (permission == null) {
            return false;
        }
        
        String editableFields = permission.getEditableFields();
        
        if (StringUtils.isBlank(editableFields)) {
            return false;
        }
        
        return Arrays.asList(editableFields.split(",")).contains(field);
    }
}

⚡ 高级功能:字段级权限

/**
 * 字段级权限控制
 */
@Aspect
@Component
public class FieldPermissionAspect {
    
    @Autowired
    private DataPermissionService dataPermissionService;
    
    /**
     * 后置处理:过滤不可见字段
     */
    @AfterReturning(value = "@annotation(dataPermission)", returning = "result")
    public void afterReturning(JoinPoint jp, DataPermission dataPermission, Object result) {
        if (result == null) {
            return;
        }
        
        UserContext currentUser = UserContextHolder.get();
        
        if (currentUser == null || currentUser.isAdmin()) {
            return;
        }
        
        // ⚡ 过滤字段
        filterFields(result, currentUser.getRoleId(), "customer");
    }
    
    /**
     * 过滤不可见字段
     */
    private void filterFields(Object result, Long roleId, String resourceType) {
        if (result instanceof List) {
            // 列表
            ((List<?>) result).forEach(item -> filterSingleObject(item, roleId, resourceType));
        } else {
            // 单个对象
            filterSingleObject(result, roleId, resourceType);
        }
    }
    
    /**
     * 过滤单个对象的字段
     */
    private void filterSingleObject(Object obj, Long roleId, String resourceType) {
        if (obj == null) {
            return;
        }
        
        Class<?> clazz = obj.getClass();
        
        for (Field field : clazz.getDeclaredFields()) {
            field.setAccessible(true);
            
            // 检查字段访问权限
            if (!dataPermissionService.hasFieldPermission(roleId, resourceType, field.getName())) {
                try {
                    // 清空不可见字段
                    field.set(obj, null);
                } catch (IllegalAccessException e) {
                    log.error("清空字段失败:field={}", field.getName(), e);
                }
            }
        }
    }
}

✅ 最佳实践

数据权限控制完整方案:

1️⃣ 权限设计:
   □ 按部门划分
   □ 按角色划分
   □ 按个人划分
   □ 支持自定义
   
2️⃣ 技术实现:
   □ MyBatis拦截器(推荐)⭐⭐⭐⭐⭐
   □ 自动拼接SQL条件
   □ 对业务代码无侵入
   
3️⃣ 性能优化:
   □ 权限配置缓存
   □ 部门树缓存
   □ 索引优化
   
4️⃣ 安全防护:
   □ SQL注入防护
   □ 权限校验
   □ 操作审计
   □ 异常告警
   
5️⃣ 扩展功能:
   □ 字段级权限
   □ 操作权限(增删改查)
   □ 时间范围权限
   □ 数据脱敏

🎉 总结

数据权限控制核心:

1️⃣ 行级过滤:自动添加SQL条件
2️⃣ 字段级控制:动态过滤敏感字段
3️⃣ 灵活配置:支持多种权限类型
4️⃣ 透明拦截:对业务代码无侵入
5️⃣ 性能优化:缓存+索引

记住:数据权限控制是企业数据安全的基石! 🔒


文档编写时间:2025年10月24日
作者:热爱数据安全的权限工程师
版本:v1.0
愿每一条数据都在掌控之中!