尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Django RBAC权限系统实战:从用户角色到接口权限的完整落地

Django RBAC权限系统实战:从用户角色到接口权限的完整落地 简介基于Django与Django REST Framework的RBAC权限管理系统示例项目面向Python开发者、毕业设计或内部管理系统二次开发场景演示通过角色-权限映射实现细粒度访问控制并以DRF提供标准RESTful API支持前后端分离模式。压缩包共97个文件主要包含38个Python后端源码、18个Vue前端组件、15个TypeScript定义另有Dockerfile、JSON配置及图片文档整体仅1.28MB目录结构清晰模块边界明确。目前已有47人学习适合作为快速搭建权限管理后台的参考实例。项目完整覆盖用户认证、角色分配、API接口开发、Docker容器化部署等关键环节并附带README说明与界面预览图可帮助开发者理解RBAC从数据模型、序列化接口到权限校验中间件的完整链路从而降低实际项目中的踩坑成本。1. 为什么 Django 权限系统要选 RBAC把 if 判断换成可配置的数据后台管理系统上线半年后最常见的失控方式是新角色加权限只能改代码运营开临时权限只能等开发审计问谁改过角色没人答得上来。RBAC 把「谁拥有什么权限」从代码里抽出来变成数据库里可配置的数据——用户挂角色角色挂权限改权限不动代码。Django 自带 auth 框架天然提供了 User、Group、Permission 三张基础表Django REST Framework 又给出认证与权限两层扩展点两者叠加就能搭出可维护的权限管理系统。接下来按示例项目包的常见结构走一遍数据模型、接口校验、前后端联动最后给出跑通与排错清单。适合做管理后台的后端工程师也适合准备权限系统面试时梳理完整链路。2. Django Model 设计User、Role、Permission 三张表怎么落地 RBAC2.1 复用内置表还是自建 Role 表两种 RBAC 落地方案的取舍RBAC 落到 Django 上第一个选择是用内置模型还是自建模型。最省事的方案是直接用 auth.Group 充当角色把 Permission 挂到 Group再用 user.groups 关联用户。这套方案零额外建表Django Admin 原生支持维护DjangoModelPermissions 也能直接识别 group 上的权限。代价是 Group 表没有 code、description 这类业务字段前端拿不到固定的角色标识而且内置 Permission 的粒度是「对某个 model 的增删改查」管不了「导出用户数据」「审核订单」这类动作级权限。多数示例项目会选折中方案用户继承 AbstractUser角色自建 Role 表权限继续复用 auth.Permission再在 Permission 之上挂菜单和按钮资源。这样既保留 Django Admin 的权限维护体验又能扩展动作级权限。自建 Role 的关键是看清反向查询名User 和 Role 是双向多对多related_name 一旦写错权限采集时拿到空集合接口全部 403。这是最隐蔽的坑后面权限类里会再次遇到。如果只是内部小工具、用户一两百用 Group 方案完全够别为理论上的优雅过度设计一旦角色要挂业务属性、前端要做按钮级控制自建 Role 表就是分水岭。2.2 最小可跑的 models.py用 AbstractUser 扩展用户并关联角色先看模型层的最小实现# rbac/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): 扩展用户模型RBAC 中用户只保留身份和基础信息权限全部通过角色间接获取 phone models.CharField(max_length20, blankTrue, default, verbose_name手机号) department models.CharField(max_length50, blankTrue, default, verbose_name部门) class Meta: verbose_name 用户 verbose_name_plural verbose_name class Role(models.Model): 角色表RBAC 的核心实体一个用户可挂多个角色一个角色可绑多个权限 name models.CharField(max_length50, verbose_name角色名称) code models.CharField(max_length50, uniqueTrue, verbose_name角色编码) permission models.ManyToManyField( auth.Permission, blankTrue, related_namerbac_roles, verbose_name权限集合 ) user models.ManyToManyField( User, blankTrue, related_namerbac_roles, verbose_name用户集合 ) class Meta: verbose_name 角色 verbose_name_plural verbose_name角色表的 code 字段是给权限判断和前端用的固定标识name 可以随时改code 定了就不要动否则前端按钮和菜单的对应关系会全断。permission 和 user 两个 ManyToManyField 是本节的骨架Django 会自动生成 rbac_role_permission 和 rbac_role_user 两张中间表不需要手写 through 表如果后续要记录「谁在什么时候给角色加了权限」再加 through 表不迟一开始加反而拖慢开发节奏。菜单不是 RBAC 三张核心表之一但管理后台的权限系统离不开它。示例项目通常把菜单作为权限的展示层资源核心字段就一个class Menu(models.Model): 菜单表用 permission_code 关联一个权限编码作为前端渲染的过滤条件 name models.CharField(max_length50, verbose_name菜单名称) path models.CharField(max_length200, verbose_name前端路由) icon models.CharField(max_length50, blankTrue, default, verbose_name图标) parent models.ForeignKey( self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren, verbose_name父级菜单 ) permission_code models.CharField( max_length50, blankTrue, default, verbose_name所需权限编码 ) sort models.IntegerField(default0, verbose_name排序)permission_code 对应 auth_permission 表里的 codename第 4 章的菜单树接口就是靠这个字段做过滤的。菜单表和权限表没有外键只用字符串关联好处是权限编码可以在管理命令里集中注册、统一管理避免菜单表里出现脏外键。用户模型写好后还要让 Django 知道用它替换默认 User# settings.py AUTH_USER_MODEL rbac.User提示AUTH_USER_MODEL 必须在第一次 migrate 之前配置好。先 migrate 生成了默认 auth_user 表再改成自定义 User迁移会直接报表冲突错误项目往往在启动阶段就失败。正确的顺序是 django创建app之后、写 models 的同时把 settings.py 配好再执行第一次迁移。2.3 makemigrations 与初始化权限数据django创建app后的关键顺序# 1. 确认 AUTH_USER_MODEL 指向 rbac.User再依次执行 python manage.py makemigrations rbac python manage.py migrate # 2. 创建管理员之后用它登录 Admin 分配角色和权限 python manage.py createsuperusermigrate 成功后每个 model 的 add/change/delete/view 内置权限会自动写入 auth_permission 表。要给「导出用户数据」这类动作建权限常见做法是写一个管理命令把业务动作注册成统一的 auth.Permission# rbac/management/commands/sync_permissions.py from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand from rbac.models import Role class Command(BaseCommand): 把动作级权限同步到 auth_permission 表重复执行不产生脏数据 help 同步业务权限 def handle(self, *args, **options): # 绑定到 Role 的内容类型上admin 权限列表里能看到归属 content_type ContentType.objects.get_for_model(Role) permissions [ (view_dashboard, 查看仪表盘), (export_user_data, 导出用户数据), (approve_order, 审核订单), (manage_role, 管理角色), ] for codename, name in permissions: Permission.objects.update_or_create( content_typecontent_type, codenamecodename, defaults{name: name}, ) self.stdout.write(self.style.SUCCESS(业务权限同步完成共 %d 条 % len(permissions)))这段命令有两个细节。第一Permission 表的唯一约束是 (content_type_id, codename) 这一对codename 本身不全局唯一所以必须固定挂在一个内容类型下否则同名 codename 会被重复插入挂在 Role 的内容类型上Admin 的权限列表里也能正常归属。第二update_or_create 让命令可重复执行角色和权限在 Admin 里怎么改都不会被覆盖。最后把 Role 和 User 注册进 Admin方便人工维护# rbac/admin.py from django.contrib import admin from django.contrib.auth.admin import UserAdmin from .models import User, Role admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display (name, code) filter_horizontal (permission, user) # 左右选择框批量分配高效直观 admin.register(User) class CustomUserAdmin(UserAdmin): fieldsets UserAdmin.fieldsets ( (扩展信息, {fields: (phone, department)}), )filter_horizontal 会把多对多字段渲染成两个左右联动选择框是 Django admin 界面美化里最实用的一行配置。用户与角色的分配在这个界面完成接口权限校验则由下一章的 DRF 权限类接管这就构成了「权限数据可配置、校验逻辑代码化」的基本闭环。如果团队习惯用代码管种子数据也可以把 sync_permissions 换成 fixtures但管理命令的好处是能对接 CI权限变更走代码 review上线时执行一遍即可。3. Django REST Framework 接口权限自定义 Permission 类接管 RBAC 校验3.1 认证先行用 simplejwt 配置登录与刷新RBAC 校验的前提是身份可靠接口层第一步是认证。示例项目常见选择是 djangorestframework-simplejwt相比 Session 认证它更契合 Django 前后端分离的部署形态Web 和移动端共用接口时也不需要处理 Cookie 跨域和 CSRF。JWT 的无状态模型把用户身份放进 token后端只负责验签省掉不少会话管理的心智负担。# settings.py REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }# 项目 urls.py from django.urls import path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/auth/login/, TokenObtainPairView.as_view(), namelogin), path(api/auth/refresh/, TokenRefreshView.as_view(), namerefresh), ]登录接口返回 access 和 refresh 两个 token后续请求在 Header 里带 Authorization: Bearer 。simplejwt 的默认参数是 access 五分钟过期、refresh 一天生产环境通常按下面这张表调整配置项默认值说明ACCESS_TOKEN_LIFETIMEtimedelta(minutes5)压短后权限变更能更快生效REFRESH_TOKEN_LIFETIMEtimedelta(days1)按业务会话长度调 17 天ROTATE_REFRESH_TOKENSFalse每次刷新都换新 refresh适合长会话UPDATE_LAST_LOGINFalse置 True 可在登录时记录 last_loginaccess 压短的意义不只是安全用户被移除角色后最迟在 access 过期时权限即失效不需要等 refresh 过期。权限变更的生效时间本质上是「access token 剩余寿命 权限类读取的数据是否最新」这两件事决定的。3.2 自定义 RbacPermission从角色集合取权限而不是默认 user_permissionsDjango 的 user.has_perm() 只查 user.user_permissions、user.groups 和 is_superuser不会查自建的 Role.permission 多对多关系。只要自建了 Role 表就不能直接用 DjangoModelPermissions得写一个自定义权限类接管判断# rbac/permissions.py from rest_framework.permissions import BasePermission class RbacPermission(BasePermission): 从用户关联的角色上收集权限编码与视图要求的 required_permission 比对。 视图里声明 required_permission app_label.codename。 def has_permission(self, request, view): # 超级管理员直接放行避免把自己锁在门外 if request.user and request.user.is_superuser: return True # 视图没声明权限要求默认放行登录即可访问的接口 required getattr(view, required_permission, None) if not required: return True app_label, codename required.split(.) # 收集当前用户所有角色的权限构成 (app_label, codename) 集合 user_perms set() roles request.user.rbac_roles.prefetch_related(permission).all() for role in roles: for perm in role.permission.all(): user_perms.add((perm.content_type.app_label, perm.codename)) return (app_label, codename) in user_perms这段逻辑里有三个值得注意的点。第一request.user 在未认证时是 AnonymousUser所以先判空再做 is_superuser 判断否则直接抛 AttributeError。第二prefetch_related 把角色和权限一次性查出来避免 N1 查询权限密集的接口如果发现数据库压力大第一步先看这里有没有懒加载。第三集合里存的是 (app_label, codename) 二元组而不是纯 codename因为 Django 内置权限的 codename 只要求在同一内容类型内唯一跨 app 时可能重名带上 app_label 才真正唯一。如果需要细化到单条数据的可见性可以在 has_object_permission 里再比对对象的部门字段但更通用的做法是放到查询集过滤见 4.3 的数据边界说明。3.3 视图与 action 绑定 required_permission给每个接口上锁有了权限类剩下的问题是让每个接口声明自己需要什么权限。常见做法是在视图中按 action 动态赋值# rbac/views.py from rest_framework import viewsets from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from .models import User from .permissions import RbacPermission from .serializers import UserSerializer class UserViewSet(viewsets.ReadOnlyModelViewSet): queryset User.objects.all() serializer_class UserSerializer def get_permissions(self): # 每个 action 绑定一个权限标识未列出的 action 走默认放行 if self.action export: self.required_permission rbac.export_user_data # 认证在前、权限在后先确认身份再做 RBAC 判断 return [IsAuthenticated(), RbacPermission()] action(detailFalse, methods[post]) def export(self, request): # 只有持有 rbac.export_user_data 的用户能走到这里 return Response({status: ok, count: self.get_queryset().count()})self.required_permission 是实例属性只影响当前请求不会串权限。export 动作必须用 action 包一层ViewSet 才能把它注册到路由不加装饰器的话post 到 /api/users/export 会命中 create 或者直接 404。如果不用 ViewSet直接在 APIView 里写 permission_classes [IsAuthenticated(), RbacPermission()] 效果一样required_permission 作为类属性声明即可ViewSet 的好处只是把同一资源的动作集中管理。每个资源用户、角色、菜单都建一个类似 ViewSet把动作和权限编码的对应关系集中写在 get_permissions 里权限清单在代码里可检索、可 review这是接口权限管理的核心。4. 前后端分离下 RBAC 权限怎么下发菜单树、按钮标识与数据边界4.1 登录后返回角色与权限集合Serializer 里合并多角色后端把校验做好还不够前端要渲染菜单和按钮必须拿到当前用户的权限集合。登录成功之后前端会再调一个 /api/auth/userinfo 接口拿用户信息这个接口的序列化器要把多对多关系拍平成数组# rbac/serializers.py from django.contrib.auth import get_user_model from rest_framework import serializers User get_user_model() class UserInfoSerializer(serializers.ModelSerializer): 登录用户信息把角色编码和权限编码合并成两个数组方便前端校验 roles serializers.SerializerMethodField() permissions serializers.SerializerMethodField() class Meta: model User fields (id, username, roles, permissions) def get_roles(self, obj): return list(obj.rbac_roles.values_list(code, flatTrue)) def get_permissions(self, obj): # 多角色权限取并集天然去重 codes set() for role in obj.rbac_roles.prefetch_related(permission).all(): codes.update(role.permission.values_list(codename, flatTrue)) # 这里可以过滤掉内置的 add/change/delete/view只留业务权限 return sorted(codes)用户挂了多个角色时权限取并集这里用 set 去重。接口返回的数据量很小角色和权限一般就几十个字符串不需要分页真正要压的是查询次数prefetch_related 在这里同样管用。如果嫌这个接口慢可以把 permissions 结果按用户缓存到 Redis角色变更时主动删 key 失效。4.2 菜单树接口按权限过滤再递归组装菜单资源也走 RBAC。菜单表里的 permission_code 字段代表「看到这个菜单需要哪个权限」接口返回树形结构侧边栏才能直接渲染# rbac/views.py from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from .models import Menu from .permissions import RbacPermission class UserMenuView(APIView): 返回当前用户可见的菜单树超级管理员直接返回全部菜单 permission_classes [IsAuthenticated, RbacPermission] def get(self, request): user_codes set() if not request.user.is_superuser: for role in request.user.rbac_roles.prefetch_related(permission).all(): user_codes.update(role.permission.values_list(codename, flatTrue)) menus Menu.objects.filter( permission_code__inuser_codes ).order_by(sort) else: menus Menu.objects.all().order_by(sort) # 先转字典再挂 children最后收集顶级节点 tree [] nodes {m.id: { id: m.id, name: m.name, path: m.path, icon: m.icon, children: [], } for m in menus} for m in menus: node nodes[m.id] parent nodes.get(m.parent_id) if parent is not None: parent[children].append(node) else: tree.append(node) return Response(tree)这段组装逻辑对子级菜单有一个坑如果父菜单没有权限而子菜单有权限子菜单的 parent 在 nodes 里找不到会被当成顶级菜单直接返回侧边栏会出现一个没有父级的悬浮项。处理办法是返回前把 parent 不在 nodes 里的菜单也过滤掉或者查询时把祖先一并带上。另一个注意点是 order_by(sort) 只保证同级菜单的查询顺序组装后 children 列表顺序和查询顺序一致不需要再单独排序。4.3 按钮级控制与数据边界RBAC 管不到的行级权限按钮级控制不需要再访问后端用 userinfo 接口返回的 permissions 数组在前端判断就够了// 前端工具函数按钮是否可用的唯一判断入口 export function hasPermission(code) { const permissions store.state.user.permissions || [] return store.state.user.is_superuser || permissions.includes(code) }在 Vue 模板里删除按钮写成el-button v-ifhasPermission(rbac.delete_user)删除/el-button即可。注意按钮判断永远对着权限编码不要对着角色编码。判断「是不是 admin」会在新增角色时迫使前端跟着改代码RBAC 的灵活性就丢了。最后要提醒 RBAC 的边界它管的是「能不能访问某个功能」管不了「能看到哪些数据」。同一份订单列表区域经理只能看自己区域的这是行级数据权限需要在查询集上按部门或数据范围过滤很多团队把它做成独立的 DataScope 机制和 RBAC 叠加使用。能意识到这两者的区别是权限系统设计里最容易拉开差距的地方。5. RBAC 示例项目最小启动命令与权限失效排查5.1 最小启动命令拿到项目包解压后先确认本机 Python 3.10 已装好并加入 PATH然后在项目根目录执行python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt python manage.py migrate python manage.py createsuperuser python manage.py runserver 0.0.0.0:8000migrate 必须放在 createsuperuser 之前否则创建用户时表还不存在。访问 http://127.0.0.1:8000/admin 登录后先建两个角色把权限挂到角色上再建一个测试用户关联其中一个角色最后用测试用户调接口验证。本地跑通后部署到服务器比如宝塔面板搭 Nginx 反代时记得把 runserver 换成 gunicornrunserver 只适合开发环境。5.2 权限不生效的排查清单现象按顺序排查所有接口 403请求头是否有 Authorization: Bearer access 是否过期加了角色仍 403related_name 是否一致是否用旧 token 请求access 未过期前权限不刷新部分接口绕过权限视图是否声明 permission_classesrequired_permission 编码是否拼写一致三个场景对应三个最常见的坑。权限校验发生在 DRF 视图的 initial() 阶段发生在认证和限流之后所以 403 先查认证再查权限不要一上来就改数据库。给测试用户分配角色后接口仍报 403先把 user.rbac_roles 反查名和 permissions.py 里用的是不是同一个 related_name 对一遍这个字段写错不会报错只会让收集到的权限集合为空。再到项目里 grep required_permission确认视图声明的编码和 sync_permissions 注册的 codename 完全一致多一个空格或大小写不同都会匹配失败。把用户权限用 UserInfoSerializer 暴露出来后前端可以用 permissions.includes() 复现一遍后端判断前后端各测一次权限问题在哪一层就清楚了。本文还有配套的精品资源点击获取
返回列表