Skip to content

数据权限

youlai-django 支持数据权限控制,实现方案与 youlai-boot 完全对齐。

基于 装饰器 + Django ORM Q 对象 的组合方案:

  • @DataPermission 装饰器:标记需要数据权限过滤的方法,支持自定义字段名
  • apply_data_permission:对 QuerySet 应用数据权限过滤
  • data_permission_required:数据权限校验入口函数

与 youlai-boot 对齐

特性youlai-bootyoulai-django
注解/装饰器@DataPermission@DataPermission
多角色策略并集(OR)策略并集(OR)策略
自定义字段名deptIdColumnName, userIdColumnNamedept_field, user_field
跳过数据权限无注解时跳过@SkipDataPermission()
超级管理员豁免ROOT 角色跳过superuser 跳过

整体架构图

数据范围枚举 DataScopeEnum

位置:core/permissions/data_scope.py

python
class DataScopeEnum:
    """数据权限范围枚举"""
    ALL = 1           # 全部数据 - 最高权限
    DEPT_AND_SUB = 2  # 本部门及子部门
    DEPT = 3          # 本部门
    SELF = 4          # 仅本人
    CUSTOM = 5        # 自定义部门

注意:枚举值越小,权限范围越大。

核心组件

1. @DataPermission 装饰器

位置:core/permissions/data_scope.py

python
class DataPermission:
    """数据权限装饰器"""
    
    def __init__(
        self,
        dept_field: str = 'dept_id',    # 部门ID字段名
        user_field: str = 'create_by'   # 用户ID字段名
    ):
        ...

2. RoleDataScope 模型

支持多角色数据权限,每个角色独立维护数据权限信息:

python
@dataclass
class RoleDataScope:
    """角色数据权限"""
    role_code: str              # 角色编码
    data_scope: int             # 数据权限范围值 (1-5)
    custom_dept_ids: List[int]  # 自定义部门ID列表(CUSTOM 时使用)

    @classmethod
    def from_role(cls, role: Role) -> 'RoleDataScope':
        """从角色创建数据权限对象"""
        ...

3. DataPermissionHandler 处理器

核心逻辑:多角色并集策略

python
class DataPermissionHandler:
    @staticmethod
    def build_q_expression(
        data_scopes: List[RoleDataScope],
        dept_field: str = 'dept_id',
        user_field: str = 'id',
        current_user_id: int = None,
        current_dept_id: int = None
    ) -> Q:
        """构建多角色并集查询条件
        
        用户有多个角色时,使用 OR 连接各角色的数据权限条件
        """
        expressions = []
        for ds in data_scopes:
            expr = self._build_role_expression(ds, ...)
            if expr is not None:
                expressions.append(expr)
        
        # OR 连接所有表达式
        result = expressions[0]
        for expr in expressions[1:]:
            result |= expr
        return result

各权限类型生成的 Q 条件

权限类型生成的 Q 条件
ALLQ() (无过滤)
DEPT_AND_SUBQ(dept_id__in=[部门及子部门ID列表])
DEPTQ(dept_id=当前部门ID)
SELFQ(create_by=当前用户ID)
CUSTOMQ(dept_id__in=[自定义部门ID列表])

使用示例

基本使用

python
from core.permissions.data_scope import (
    DataPermission, 
    apply_data_permission,
    data_permission_required
)

class UserViewSet(viewsets.ModelViewSet):
    
    # 方式一:装饰器 + apply_data_permission
    @DataPermission(dept_field='dept_id', user_field='create_by')
    def list(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        # 应用数据权限过滤
        queryset = apply_data_permission(
            queryset, 
            request.user,
            dept_field='dept_id',
            user_field='create_by'
        )
        return self.get_paginated_response(...)
    
    # 方式二:直接使用 data_permission_required
    def destroy(self, request, *args, **kwargs):
        target_user = self.get_object()
        # 检查是否有权限删除目标用户
        if not data_permission_required(request.user, "delete", target_user.id):
            raise PermissionDenied("无权限删除此用户")
        return super().destroy(request, *args, **kwargs)

自定义字段名

python
# 任务表使用不同的字段名
@DataPermission(dept_field='org_id', user_field='creator_id')
def get_task_list(self, request):
    queryset = Task.objects.filter(is_deleted=0)
    queryset = apply_data_permission(
        queryset, 
        request.user,
        dept_field='org_id',
        user_field='creator_id'
    )
    return Response(queryset.values())

跳过数据权限

python
from core.permissions.data_scope import SkipDataPermission

class ConfigViewSet(viewsets.ModelViewSet):
    
    @SkipDataPermission()
    def list(self, request, *args, **kwargs):
        # 不应用数据权限过滤
        return super().list(request, *args, **kwargs)

多角色数据权限

用户拥有多个角色时,采用并集策略(OR 连接)

python
# 用户拥有两个角色:
# - 部门管理员: dataScope = 3 (本部门)
# - 普通用户: dataScope = 4 (本人)

# 生成的 Q 条件:
Q(dept_id=10) | Q(create_by=100)

# 用户可以看到本部门的数据 OR 自己创建的数据

获取可查看用户范围

python
from core.permissions.data_scope import get_viewable_user_ids

def list(self, request):
    # 返回 "all" 或用户ID列表
    viewable_ids = get_viewable_user_ids(request.user)
    
    if viewable_ids != "all":
        queryset = queryset.filter(create_by__in=viewable_ids)
    
    return Response(queryset.values())

判断是否有权限操作目标用户

python
from core.permissions.data_scope import has_permission_to_user

def update(self, request, *args, **kwargs):
    target_user = self.get_object()
    
    if not has_permission_to_user(request.user, target_user.id):
        raise PermissionDenied("无权限操作此用户")
    
    return super().update(request, *args, **kwargs)

数据库设计

sys_role 表

sql
CREATE TABLE `sys_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `code` varchar(32) NOT NULL COMMENT '角色编码',
  `data_scope` tinyint COMMENT '数据权限(1-全部 2-部门及子部门 3-本部门 4-本人 5-自定义)',
  PRIMARY KEY (`id`)
);

sys_role_dept 表(自定义权限)

sql
CREATE TABLE `sys_role_dept` (
  `role_id` bigint NOT NULL COMMENT '角色ID',
  `dept_id` bigint NOT NULL COMMENT '部门ID',
  UNIQUE INDEX `uk_roleid_deptid`(`role_id`, `dept_id`)
);

权限模块结构

core/permissions/
├── __init__.py
├── perms.py           # 接口权限类 (HasPerm)
├── decorators.py      # 权限装饰器 (permission_required)
└── data_scope.py      # 数据权限模块
    ├── DataScopeEnum          # 数据权限枚举
    ├── RoleDataScope          # 角色数据权限模型
    ├── DataPermissionHandler  # 数据权限处理器
    ├── DataPermission         # 数据权限装饰器
    ├── SkipDataPermission     # 跳过数据权限装饰器
    ├── apply_data_permission  # 应用数据权限函数
    ├── get_viewable_user_ids  # 获取可查看用户ID
    ├── has_permission_to_user # 判断是否有权限操作用户
    └── data_permission_required # 数据权限校验入口

注意事项

  1. 超级管理员自动跳过user.is_superuser == True 不受数据权限限制
  2. 必须手动调用 apply_data_permission:装饰器只是注入配置,需要在 QuerySet 上调用过滤函数
  3. 使用 @SkipDataPermission 跳过:某些场景需要跳过时可使用此装饰器
  4. 并集策略:多角色时权限取并集,用户能看到所有角色权限范围内的数据
  5. 枚举值对齐:与 youlai-boot 保持一致,确保前后端数据权限值匹配

基于 MIT 许可发布