下载工作台
Django Web 开发

RBAC 与对象级权限

试读上半部分 · 解锁后可读全文

第 16 章 · RBAC 与对象级权限

本章目标:在 shop-demo 毕业项目基础上,实现 RBAC(基于角色的访问控制)对象级权限;对比 Django 内置 Group / Permission自定义 Role 模型;使用 django-guardian 或自研 assign_perm / has_perm;编写 IsProductOwnerRoleBasedPermission 等 DRF 权限类与视图装饰器;落地 admin / editor / viewer 三角色种子数据;保证 Admin 后台与 REST API 统一权限检查;与 Flask ch16 RBAC 对照。

学时建议:5~6 小时(含 2 小时权限联调)

前置:本模块 ch03 认证与权限、ch07 DRF、ch12 shop-demo MVP;ch15 SimpleJWT 可选(API 与 Session 可并存)。


16.1 为什么需要 RBAC

ch12 毕业项目仅区分「已登录 / 未登录」。运营团队扩大后需细分能力:

角色典型能力
admin用户管理、全量商品、系统配置
editor创建/编辑商品、上传封面
viewer只读列表与详情
┌─────────┐  M:N  ┌─────────┐  M:N  ┌────────────┐
│  User   │◄────►│  Role   │◄────►│ Permission │
└────┬────┘      └─────────┘      └────────────┘
     │ 1:N
     ▼
┌─────────┐  对象级:created_by_id == user.id
│ Product │
└─────────┘
shop-demo 为教学项目;权限码采用 资源:动作(如 product:write),不映射任何真实业务系统。域名统一使用 user-demo.example.comapi.example.com

16.2 Django Group/Permission 进阶 vs 自定义 Role

Django 内置权限绑定 ContentType(模型),格式为 app_label.codename

内置能力说明
auth.Group用户组,批量赋权
auth.Permissioncatalog.change_product
user.groupsUser ↔ Group 多对多
user.user_permissions直接赋权(少用)
@permission_required('catalog.change_product')视图装饰器
{% if perms.catalog.change_product %}模板判断

内置方案适用:标准 CRUD、Django Admin 自动挂钩、权限与模型一一对应。

自定义 Role 模型适用:权限码与 OpenAPI(ch14)对齐、跨模型业务动作(如 order:refund)、与前端菜单配置驱动。

accounts/models.py(自定义 Role,与内置 Group 二选一或并存):

from django.conf import settings
from django.db import models

class Permission(models.Model):
    """业务权限码,非 django.contrib.auth.Permission"""
    code = models.CharField(max_length=64, unique=True)
    description = models.CharField(max_length=128, blank=True)

    def __str__(self):
        return self.code

class Role(models.Model):
    name = models.CharField(max_length=32, unique=True)   # admin / editor / viewer
    label = models.CharField(max_length=64)
    permissions = models.ManyToManyField(Permission, blank=True)

    def __str__(self):
        return self.label

class UserRole(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    role = models.ForeignKey(Role, on_delete=models.CASCADE)

    class Meta:
        unique_together = ("user", "role")

扩展 User(accounts/models.pyaccounts/services/permissions.py):

def user_has_permission(user, code: str) -> bool:
    if not user.is_authenticated:
        return False
    if user.is_superuser:
        return True
    return UserRole.objects.filter(
        user=user, role__permissions__code=code
    ).exists()

def user_has_role(user, name: str) -> bool:
    return UserRole.objects.filter(user=user, role__name=name).exists()
维度Django 内置 Group/Permission自定义 Role
权限命名catalog.change_productproduct:write
Admin 集成开箱即用需自定义 ModelAdmin
迁移成本需建表与种子
业务语义偏模型 CRUD偏 API / 菜单动作
推荐小团队、Admin 为主API 为主、多前端

shop-demo 建议:保留内置 Group 给 Django Admin 超管;业务后台与 DRF 走自定义 Role + Permission.code,避免两套语义混用。


16.3 对象级权限:django-guardian 与自研

功能权限(能否调用编辑接口)与对象权限(能否编辑这一条记录)需分层。

方案 A:django-guardian

pip install django-guardian

settings.py

INSTALLED_APPS += ["guardian"]
AUTHENTICATION_BACKENDS = (
    "django.contrib.auth.backends.ModelBackend",
    "guardian.backends.ObjectPermissionBackend",
)
ANONYMOUS_USER_NAME = None

catalog/models.pyProduct 注册对象权限:

from guardian.shortcuts import assign_perm, get_objects_for_user

# 视图中
assign_perm("change_product", editor_user, product)
assign_perm("view_product", viewer_user, product)

# 查询可编辑商品
editable = get_objects_for_user(
    request.user, "catalog.change_product", Product, accept_global_perms=False
)

核心 API:assign_perm / remove_permuser.has_perm(perm, obj)get_objects_for_user(...) 过滤 QuerySet。

方案 B:自研(基于 created_by

catalog/models.py

class Product(models.Model):
    # ... 其他字段
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True, blank=True,
        on_delete=models.SET_NULL,
        related_name="created_products",
    )

accounts/services/object_perms.py

def can_edit_product(user, product) -> bool:
    if user.is_superuser or user_has_role(user, "admin"):
        return True
    if user_has_permission(user, "product:write"):
        return product.created_by_id == user.id
    return False

def products_for_user(user):
    qs = Product.objects.all()
    if user.is_superuser or user_has_role(user, "admin"):
        return qs
    if user_has_permission(user, "product:read"):
        return qs.filter(
            models.Q(created_by=user) | models.Q(is_published=True)
        )
    return qs.none()
方案优点缺点
guardian成熟、与 has_perm(obj) 统一多表、权限与 ContentType 耦合
自研轻量、规则清晰复杂规则需自行扩展

shop-demo 教学默认采用 自研 + created_by;若需「把某商品共享给另一编辑」再引入 guardian。


16.4 装饰器与 DRF Permission Class

视图装饰器(模板后台)

accounts/decorators.py

from functools import wraps
from django.contrib.auth.decorators import login_required
from django.core.exceptions import PermissionDenied
from accounts.services.permissions import user_has_permission

def permission_required(code: str):
    def decorator(view_func):
        @login_required
        @wraps(view_func)
        def _wrapped(request, *args, **kwargs):
            if not user_has_permission(request.user, code):
                raise PermissionDenied("权限不足")
            return view_func(request, *args, **kwargs)
        return _wrapped
    return decorator

视图组合:@permission_required("product:write") + can_edit_product(request.user, product) 对象校验。

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。