第 16 章 · RBAC 与对象级权限
本章目标:在 shop-demo 毕业项目基础上,实现 RBAC(基于角色的访问控制) 与对象级权限;对比 Django 内置 Group / Permission 与自定义 Role 模型;使用 django-guardian 或自研 assign_perm / has_perm;编写 IsProductOwner、RoleBasedPermission 等 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.com、api.example.com。
16.2 Django Group/Permission 进阶 vs 自定义 Role
Django 内置权限绑定 ContentType(模型),格式为 app_label.codename:
| 内置能力 | 说明 |
|---|---|
auth.Group | 用户组,批量赋权 |
auth.Permission | 如 catalog.change_product |
user.groups | User ↔ 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.py 或 accounts/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_product | product: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.py 为 Product 注册对象权限:
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_perm、user.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) 对象校验。