数据权限
youlai-django 支持数据权限控制,实现方案与 youlai-boot 完全对齐。
基于 装饰器 + Django ORM Q 对象 的组合方案:
- @DataPermission 装饰器:标记需要数据权限过滤的方法,支持自定义字段名
- apply_data_permission:对 QuerySet 应用数据权限过滤
- data_permission_required:数据权限校验入口函数
与 youlai-boot 对齐
| 特性 | youlai-boot | youlai-django |
|---|---|---|
| 注解/装饰器 | @DataPermission | @DataPermission |
| 多角色策略 | 并集(OR)策略 | 并集(OR)策略 |
| 自定义字段名 | deptIdColumnName, userIdColumnName | dept_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 条件 |
|---|---|
| ALL | Q() (无过滤) |
| DEPT_AND_SUB | Q(dept_id__in=[部门及子部门ID列表]) |
| DEPT | Q(dept_id=当前部门ID) |
| SELF | Q(create_by=当前用户ID) |
| CUSTOM | Q(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 # 数据权限校验入口注意事项
- 超级管理员自动跳过:
user.is_superuser == True不受数据权限限制 - 必须手动调用 apply_data_permission:装饰器只是注入配置,需要在 QuerySet 上调用过滤函数
- 使用 @SkipDataPermission 跳过:某些场景需要跳过时可使用此装饰器
- 并集策略:多角色时权限取并集,用户能看到所有角色权限范围内的数据
- 枚举值对齐:与 youlai-boot 保持一致,确保前后端数据权限值匹配
