Skip to content

数据权限

数据权限是一种行级安全控制机制,用于限制用户只能访问其权限范围内的数据行。与功能权限(控制用户能执行哪些操作)不同,数据权限控制用户能看到哪些数据。

设计目标

目标说明
细粒度控制按组织架构维度控制数据可见范围
多角色支持用户拥有多个角色时,采用并集策略合并权限
无侵入实现基于MyBatis-Plus拦截器自动注入SQL,业务代码无感知
灵活配置支持全部、部门层级、个人、自定义等多种范围

数据范围类型

类型说明
1全部数据可访问系统所有数据
2本部门及子部门可访问用户所属部门及其下级部门的数据
3本部门仅可访问用户所属部门的数据
4本人仅可访问自己创建的数据
5自定义可访问指定部门的数据(需配置部门列表)

核心组件

注解定义

java
@DataPermission(
    deptAlias = "d",           // 部门表别名
    deptIdColumnName = "dept_id",  // 部门ID字段名
    userAlias = "u",           // 用户表别名
    userIdColumnName = "create_by" // 用户ID字段名
)
List<User> listUsers();
属性说明默认值
deptAlias部门表别名(多表查询时使用)""
deptIdColumnName部门ID字段名"dept_id"
userAlias用户表别名(多表查询时使用)""
userIdColumnName用户ID字段名"create_by"

核心处理器

MyDataPermissionHandler 实现了 DataPermissionHandler 接口,核心逻辑:

java
public Expression getSqlSegment(Expression where, String mappedStatementId) {
    // 1. 跳过超级管理员
    if (SecurityUtils.isRoot()) {
        return where;
    }
    
    // 2. 获取用户多角色数据权限
    List<RoleDataScope> roleDataScopes = SecurityUtils.getUser().getRoleDataScopes();
    
    // 3. 任一角色是ALL,跳过过滤
    if (hasAllDataScope(roleDataScopes)) {
        return where;
    }
    
    // 4. 构建并集过滤条件
    return dataScopeFilterWithUnion(annotation, roleDataScopes, where);
}

多角色权限合并策略

当用户拥有多个角色时,采用并集(OR)策略合并数据权限:

合并规则

  1. ALL优先规则:任一角色拥有"全部数据"权限时,跳过所有过滤
  2. OR连接规则:各角色的过滤条件通过 OR 连接,取并集
  3. 条件隔离规则:各角色的条件用括号包裹,避免逻辑歧义

实现代码

java
private Expression dataScopeFilterWithUnion(DataPermission annotation, 
                                            List<RoleDataScope> roleDataScopes, 
                                            Expression where) {
    Expression unionExpression = null;
    
    for (RoleDataScope scope : roleDataScopes) {
        Expression roleExpression = buildRoleDataScopeExpression(scope);
        if (roleExpression != null) {
            // OR连接各角色条件
            unionExpression = (unionExpression == null) 
                ? roleExpression 
                : new OrExpression(unionExpression, roleExpression);
        }
    }
    
    // 括号包裹并集条件
    return new AndExpression(where, new Parenthesis(unionExpression));
}

自定义部门权限

数据库设计

sql
-- 角色表增加数据权限字段
ALTER TABLE sys_role ADD COLUMN data_scope TINYINT DEFAULT 1 
    COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)';

-- 角色部门关联表(自定义权限使用)
CREATE TABLE sys_role_dept (
    role_id BIGINT NOT NULL COMMENT '角色ID',
    dept_id BIGINT NOT NULL COMMENT '部门ID',
    PRIMARY KEY (role_id, dept_id)
) COMMENT '角色部门关联表';

后端实现

java
// 保存角色时处理自定义部门
@Transactional
public void saveRole(RoleForm form) {
    // 保存角色基本信息
    roleMapper.insert(role);
    
    // 如果是自定义权限,保存角色部门关联
    if (DataScopeEnum.CUSTOM.getValue().equals(form.getDataScope())) {
        roleDeptService.saveRoleDepts(role.getId(), form.getDeptIds());
    }
}

// 查询用户数据权限时获取自定义部门
public List<RoleDataScope> getRoleDataScopes(Set<String> roleCodes) {
    return roleMapper.getRoleDataScopeList(roleCodes).stream()
        .map(map -> {
            List<Long> customDeptIds = null;
            if (DataScopeEnum.CUSTOM.getValue().equals(map.get("data_scope"))) {
                customDeptIds = roleDeptService.getDeptIdsByRoleId(map.get("role_id"));
            }
            return new RoleDataScope(map.get("code"), map.get("data_scope"), customDeptIds);
        })
        .collect(Collectors.toList());
}

JWT Token 存储

多角色数据权限存储在JWT Token中,避免每次查询数据库:

json
{
  "userId": 1,
  "deptId": 10,
  "authorities": ["ROLE_ADMIN", "ROLE_USER"],
  "roleDataScopes": [
    {
      "roleCode": "ADMIN",
      "dataScope": 1,
      "customDeptIds": null
    },
    {
      "roleCode": "MANAGER",
      "dataScope": 5,
      "customDeptIds": [20, 30, 40]
    }
  ]
}

使用指南

基本用法

java
@Service
public class UserServiceImpl {
    
    // 在Mapper方法上添加注解
    @DataPermission
    List<User> listUsers();
    
    // 多表查询时指定别名
    @DataPermission(deptAlias = "u", userAlias = "u")
    List<Order> listOrders();
}

复杂查询场景

java
// 场景:订单表关联用户表,按用户所属部门过滤
@DataPermission(
    deptAlias = "u",           // 用户表别名
    deptIdColumnName = "dept_id"
)
@Select("""
    SELECT o.* FROM sys_order o
    LEFT JOIN sys_user u ON o.user_id = u.id
    WHERE o.status = #{status}
""")
List<Order> listOrdersByStatus(Integer status);

注意事项

  1. 字段一致性:确保数据库字段名与注解配置一致
  2. 别名使用:多表JOIN查询时必须指定正确的表别名
  3. 性能考量:部门层级查询使用 FIND_IN_SET,大数据量时考虑缓存部门树
  4. 权限绕过:超级管理员(ROOT角色)自动跳过数据权限过滤

最佳实践

场景建议方案
普通业务表默认配置,使用 dept_idcreate_by 字段
多表关联查询明确指定表别名和字段名
高并发场景缓存部门树结构,避免子查询
特殊业务逻辑不添加 @DataPermission 注解,手动控制

数据库表结构参考

sql
-- 角色表
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(64) NOT NULL COMMENT '角色名称',
    code VARCHAR(64) NOT NULL COMMENT '角色编码',
    data_scope TINYINT DEFAULT 1 COMMENT '数据权限范围',
    -- ... 其他字段
);

-- 角色部门关联表
CREATE TABLE sys_role_dept (
    role_id BIGINT NOT NULL COMMENT '角色ID',
    dept_id BIGINT NOT NULL COMMENT '部门ID',
    PRIMARY KEY (role_id, dept_id)
);

-- 部门表(需包含树路径字段)
CREATE TABLE sys_dept (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(64) NOT NULL COMMENT '部门名称',
    parent_id BIGINT COMMENT '父部门ID',
    tree_path VARCHAR(255) COMMENT '祖先节点路径,如:1,2,3',
    -- ... 其他字段
);

相关文件

文件路径说明
common/enums/DataScopeEnum.java数据权限枚举定义
common/annotation/DataPermission.java数据权限注解
plugin/mybatis/MyDataPermissionHandler.java核心处理器
security/model/RoleDataScope.java角色数据权限模型
system/service/RoleService.java角色服务接口
system/service/RoleDeptService.java角色部门关联服务

基于 MIT 许可发布