标题: 数据权限还在代码里写死?动态权限来了!
副标题: 从行级过滤到部门隔离,数据安全全方位防护
🎬 开篇:一次数据泄露的严重事故
某企业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
愿每一条数据都在掌控之中! ✨