ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django + DRF构建RBAC权限管理系统,实现前后端分离的认证与授权

Django + DRF构建RBAC权限管理系统,实现前后端分离的认证与授权 简介基于Django与DRF框架构建的RBAC权限管理系统示例项目面向需要快速掌握权限建模、接口开发及前后端分离的Python工程师。项目围绕角色、用户、权限三类核心模型实现了用户认证、自定义权限校验、异常处理、访问控制中间件及基于Vue的前端管理界面覆盖从数据库设计到接口暴露再到页面交互的完整流程适合作为企业后台权限模块的参考模板。压缩包共97个文件以Python源码、Vue组件、TypeScript定义和JSON配置为主同时包含Docker部署文件、依赖清单和说明文档整体仅1.28MB其中后端核心、认证模块、业务应用等目录划分清晰便于按功能模块阅读和二次开发。整个示例提供前后端分离的工程结构开发者能以较低成本启动项目并通过完整的RBAC授权流程、DRF视图与序列化器用法、前端路由与请求封装理解权限系统从数据层到表现层的落地方式。已有47人浏览学习适合具备Django基础并希望掌握权限控制实战的中级开发者。 我接过不少内部系统的权限改造最常见的问题是权限判断散落在 Django 视图函数里处处if user.is_authenticated and user.id obj.user_id。改一次角色规则恨不得把所有视图翻一遍。这个示例项目把权限判断从视图里剥出来用 RBAC 的角色、权限关系表结合 Django REST Framework 的自定义认证与权限校验类在请求进入视图前完成鉴权。项目还带了一套 Vite TypeScript 前端能够直接跑通登录、菜单、接口授权整个链路。适合正在做管理后台的 Django 开发尤其适合准备把老系统从逻辑散装权限收敛成模型化权限的团队。下面按模型设计、认证链路、前后端联调和查询优化来拆。2. 数据模型与权限分配核心表结构从 AbstractUser 到 ManyToManyField2.1 为什么把权限拆成五张表而不是塞进用户表RBAC 的核心是不让用户和权限直接绑定。用户只持有角色角色持有权限。这样新增一个“教务管理员”时不用逐个人改权限只要新建角色、挂上权限再把角色分配给用户。权限系统的可维护性来自这一层间接关系。Django 的AbstractUser提供了用户名、密码、邮箱、is_staff等基础字段但不会替你设计角色和权限的关系。项目的apps/rbac目录下通常维护了下面几类数据表表用途关键关联sys_user用户基础信息继承 AbstractUser与 Role 多对多sys_role角色如 admin、teacher与 User/Permission 多对多sys_permission权限点通常对应一个接口与 Role 多对多sys_user_role用户-角色中间表外键 user_id, role_idsys_role_permission角色-权限中间表外键 role_id, permission_idDjango 的 ManyToManyField 会自动生成中间表项目里显式定义这两张中间表的好处是可以在中间表上扩展额外字段比如角色的生效时间、权限的适用范围。对于只做登录授权的项目让 Django 自动建表也足够但显式中间表在排查roles__permissions这样的跨表查询时更直观。2.2 模型代码示例与迁移细节在apps/rbac/models.py中用户模型可以写成这样from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone models.CharField(max_length20, blankTrue, nullTrue) roles models.ManyToManyField( Role, throughUserRole, related_nameusers, blankTrue ) class Meta: db_table sys_user class Role(models.Model): name models.CharField(max_length50) code models.CharField(max_length50, uniqueTrue) description models.CharField(max_length255, blankTrue) class Meta: db_table sys_role class Permission(models.Model): name models.CharField(max_length50) code models.CharField(max_length100, uniqueTrue, db_indexTrue) method models.CharField(max_length10, blankTrue) url models.CharField(max_length255, blankTrue) class Meta: db_table sys_permission class UserRole(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) role models.ForeignKey(Role, on_deletemodels.CASCADE) assigned_at models.DateTimeField(auto_now_addTrue) class Meta: db_table sys_user_role unique_together (user, role) class RolePermission(models.Model): role models.ForeignKey(Role, on_deletemodels.CASCADE) permission models.ForeignKey(Permission, on_deletemodels.CASCADE) grant_at models.DateTimeField(auto_now_addTrue) class Meta: db_table sys_role_permission unique_together (role, permission)这里的Permission并没有做成 Django 内置 Permission 的扩展而是独立的一张表。原因是内置 Permission 的codename偏向代码权限点而项目里的权限需要和接口的 method、url 对应放独立表才能让权限验证逻辑直接读request.path和request.method。db_indexTrue加在Permission.code上因为后续权限校验时最常按code查询。如果用 DRF 的序列化输出用户菜单related_nameusers也能让role.users反向查询更顺手。迁移时如果数据库是 MySQL项目根目录的manage.py会要求先装驱动常见做法是pip install mysqlclient但这一步在 python3.10 以上的环境里偶尔会编译失败后面第 5 章专门讲怎么处理。先执行python manage.py makemigrations rbac python manage.py migratemakemigrations只检测模型变化migrate才会把表真正建到数据库。新增了through的多对多字段后Django 不会再自动建中间表而是以你显式定义的UserRole和RolePermission为准所以这两个模型最好放在一个迁移文件里避免迁移依赖顺序问题。2.3 初始化角色和权限的脚本权限点只在启动时初始化一次不要写在视图里。项目里常见做法是用 Django 的 management command。from django.core.management.base import BaseCommand from rbac.models import Role, Permission class Command(BaseCommand): help 初始化角色与权限 def handle(self, *args, **options): perms [ {name: 学生列表, code: student:list, method: GET, url: /api/students/}, {name: 学生详情, code: student:detail, method: GET, url: /api/students/{id}/}, {name: 创建学生, code: student:create, method: POST, url: /api/students/}, ] for item in perms: Permission.objects.get_or_create(codeitem[code], defaultsitem) admin, _ Role.objects.get_or_create(codeadmin, defaults{name: 管理员}) admin.permissions.set(Permission.objects.all()) self.stdout.write(self.style.SUCCESS(初始化完成))保存后执行python manage.py init_permissions脚本里用get_or_create保证幂等重复执行不会产生重复权限。set方法会先清空角色原权限再写入所有权限适合初始化。如果是增量调整就要改用add或remove。这里给权限编码定一个规范模块:动作例如student:create。编码越短越好维护后续在权限验证时可以直接和resolve(request.path)出来的视图名做对照。别把 URL 直接当 codeURL 一改动所有角色都要跟着改。3. TokenAuthentication 与 RbacPermissionVerification认证、鉴权与异常处理的完整链路3.1 Token 认证直接复用 DRF TokenAuthentication 还是自定义DRF 自带的TokenAuthentication使用rest_framework.authtoken模型的 Token一个用户对应一个 token登录时刷新 token 需要手动删除重建。它的问题是 token 只存在内存侧没有过期时间适合内部管理系统不适合对外公网 API。示例项目的core/TokenAuthentication.py本质上是基于 DRF 认证基类的自定义实现我们可以把它理解成一个可插拔的认证器。常见写法如下from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed from rest_framework.authtoken.models import Token class TokenAuthentication(BaseAuthentication): keyword Token def authenticate(self, request): auth_header request.META.get(HTTP_AUTHORIZATION, ) if not auth_header.startswith(self.keyword): return None key auth_header.split( )[1].strip() token Token.objects.select_related(user).filter(keykey).first() if token is None: raise AuthenticationFailed(无效或过期的登录凭证) return (token.user, token)authenticate返回的是一个二元组(user, auth)DRF 会把它写入request.user和request.auth。select_related(user)是为了避免每次请求多查一次用户表token 表有外键join 一次就能拿到完整用户对象。头部写成Token而不是Bearer是为了和 DRF 默认约定对齐前端 axios 封装时也统一用这个前缀。3.2 基于 resolve 的 RBAC 权限校验权限校验类core/RbacPermissionVerification.py要解决的问题是拿到用户后如何判断这个用户允许访问当前 URL。逐个if判断太笨我一般会把权限编码设计成和视图名对齐再用 Django 的resolve反射出当前视图名。from rest_framework.permissions import BasePermission from django.urls import resolve class RbacPermissionVerification(BasePermission): def has_permission(self, request, view): if not request.user or not request.user.is_authenticated: return False if request.user.is_superuser: return True match resolve(request.path) view_name match.url_name or view.__class__.__name__ perm_code f{request.method.lower()}:{view_name} return request.user.roles.filter( permissions__codeperm_code ).exists()这里没有用request.user.has_perm因为 Django 原生权限没有处理GET /students/和POST /students/的差异。把 method 放进权限编码后同一张表的前端列表和新建按钮就能拆成两个权限点。配合resolve需要路由里定义name。比如path(api/students/, StudentListView.as_view(), namestudent-list)这样resolve(/api/students/)会返回url_namestudent-list权限编码就可以约定成GET:student-list比在视图里写死字符串更不容易漂移。3.3 ExceptionHandler统一返回结构DRF 默认异常信息是{detail: ...}前端处理起来不够统一。项目的core/ExceptionHandler.py就是做映射的把认证失败、权限不足、参数校验失败都格式化成{code, message}结构。from rest_framework.views import exception_handler from rest_framework.response import Response from rest_framework import status as http_status def custom_exception_handler(exc, context): response exception_handler(exc, context) if response is None: return Response({ code: 500, message: 服务端处理异常 }, statushttp_status.HTTP_500_INTERNAL_SERVER_ERROR) detail response.data.get(detail) if detail in response.data else response.data return Response({ code: response.status_code, message: detail }, statusresponse.status_code)exception_handler(exc, context)会先交给 DRF 处理能识别的异常会返回一个 response识别不了的会返回 None。这里保留 DRF 的响应状态码只改 body 结构。前端拦截器收到 401 时可以直接跳登录收到 403 时提示无权限。注意不要把真实的异常堆栈直接拼到 message 里内部错误详情要写到日志文件。3.4 settings 中的全局挂载与配置优先级把上面三个类挂到REST_FRAMEWORK配置里整个项目所有接口就自动启用REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ core.TokenAuthentication.TokenAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ core.RbacPermissionVerification.RbacPermissionVerification, ], EXCEPTION_HANDLER: core.ExceptionHandler.custom_exception_handler, }配置项作用影响范围DEFAULT_AUTHENTICATION_CLASSES全局认证方式每个 APIView 默认执行DEFAULT_PERMISSION_CLASSES全局权限校验每个 APIView 默认执行EXCEPTION_HANDLER全局异常格式化所有视图抛出的异常配置后如果某个接口希望匿名可访问比如登录接口需要单独设置permission_classes [AllowAny]。视图类上的属性会覆盖全局配置这是 DRF 设置优先级的固定规则。建议在core目录下建一个exempt_urls.py集中维护不需要登录的路径避免散落在各个视图。4. 前后端分离联调与部署Vite 前端、Dockerfile 与宝塔部署 Django4.1 Vite TypeScript 前端的 axios 封装项目里的frontend目录是 Vite TypeScript Tailwind这类前端工程和后端只通过 JSON 交互。登录成功后后端返回 token前端存在localStorage之后每个请求在 axios 拦截器里加上 Authorization 头。import axios from axios const api axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api }) api.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Token ${token} } return config }) api.interceptors.response.use( response response.data, error { const status error.response?.status if (status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default apiimport.meta.env.VITE_API_BASE来自.env文件不同环境可以配不同的后端地址。开发时 Vite 启动在 5173 端口后端跑在 8000 端口需要 Vite proxy 把/api转发到 8000否则会有跨域问题。// vite.config.ts export default defineConfig({ server: { proxy: { /api: http://localhost:8000 } } })4.2 Dockerfile 构建示例项目根目录有Dockerfile对于 Django 后端常见做法是直接用 Python 官方镜像。需要把 requirements.txt 先拷贝进去这样依赖层可以缓存后端代码改动时不用重新装依赖。FROM python:3.11-slim WORKDIR /app ENV PYTHONDONTWRITEBYTECODE1 \ PYTHONUNBUFFERED1 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple COPY . . EXPOSE 8000 CMD [sh, -c, python manage.py migrate python manage.py runserver 0.0.0.0:8000]启动命令里先执行 migrate 再启动服务适合单机部署。如果是生产环境需要换成 gunicorngunicorn drfRbac.wsgi:application --bind 0.0.0.0:8000 --workers 3drfRbac.wsgi:application对应项目里的drfRbac/wsgi.py。workers 数量不是越多越好一般按 CPU 核数 * 2 1 估算。如果部署后静态文件 404需要执行python manage.py collectstatic并把STATIC_ROOT指向 nginx 能访问的目录。4.3 宝塔部署 Django 的落地流程很多团队的生产环境是宝塔面板 Nginx。宝塔部署 Django 的关键点在于用python3 -m venv创建虚拟环境在虚拟环境里装依赖再用 uwsgi 或 gunicorn 启动最后把 Nginx 反向代理到 8000 端口。# linux系统安装python后进入项目目录 python3 -m venv /www/wwwroot/drfRbac/venv source /www/wwwroot/drfRbac/venv/bin/activate pip install -r requirements.txt python manage.py migrate python manage.py collectstatic --noinput # 测试启动 gunicorn drfRbac.wsgi:application --bind 127.0.0.1:8000Nginx 配置的核心片段location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }X-Forwarded-For很重要否则 Django 的request.META[REMOTE_ADDR]拿到的全是 Nginx 的 IP审计登录日志时看不到真实客户端。宝塔部署的时候还要在安全组里放行 8000 端口或者直接不对外暴露只让 Nginx 走内网访问。4.4 前后端联调时容易漏的字段联调时最容易遇到的坑是 token 的 key 命名不一致、权限编码对不上、时间格式不统一。建议后端把认证相关字段统一成一个结构返回。{ code: 0, message: ok, data: { token: xxxx, user: { id: 1, username: admin, roles: [admin], permissions: [student:list, student:create] } } }前端路由守卫一般根据permissions里的编码决定显示哪些菜单。后端千万不要把权限校验完全交给前端permissions列表只是用来渲染界面真正拦截要在RbacPermissionVerification里做。前后端分离项目的权限安全边界永远在后端。5. 权限查询优化与 Django 管理后台实用技巧5.1 用 prefetch_related 避免权限查询 N1用户登录后前端通常一次性返回角色和权限列表如果按user.roles.all()然后每个角色再查权限会产生大量 SQL。Django 提供prefetch_related解决多对多关系查询from django.contrib.auth import get_user_model User get_user_model() def user_permissions(user_id): user User.objects.filter(iduser_id).prefetch_related(roles__permissions).first() if not user: return [] return list(user.roles.values_list(permissions__code, flatTrue).distinct())prefetch_related(roles__permissions)会把用户角色、角色权限分两条 SQL 一次性查出来再在内存里组装避免循环查询。values_list配合distinct()可以去掉重复权限因为多个角色可能持有同一个权限点。5.2 Django admin 界面美化的轻量做法示例项目里包含rbac这个 app后台管理可以注册进 Django admin。默认 admin 界面比较朴素把多对多选择框改成双栏就能明显提升操作效率from django.contrib import admin from .models import Role, Permission, User admin.register(Role) class RoleAdmin(admin.ModelAdmin): list_display (name, code, description) search_fields (name, code) filter_horizontal (permissions,)filter_horizontal是 Django 自带的双栏选择组件适用于ManyToManyField。search_fields会自动生成搜索框数据量大的时候要确认对应字段有索引。admin 的定位是管理后台、内部运维不要直接暴露到公网配合上一章的 Nginx 配置只允许内网访问。5.3 部署时的常见报错与处理在 linux 上安装 python 和依赖时最容易卡在pip install mysqlclient。这个库编译需要 MySQL 开发头文件Debian/Ubuntu 下运行apt-get install default-libmysqlclient-dev build-essential pip install mysqlclient如果不想装系统依赖也可以在requirements.txt里改用pymysql然后在drfRbac/settings.py里加import pymysql pymysql.install_as_MySQLdb()这个兼容方案适合快速部署但性能上优先建议用mysqlclient。DRF 升级到 3.15 之后部分旧版BasePermission的message字段格式有调整出现 403 响应异常时优先看ExceptionHandler里是否把detail正常提取出来。权限系统最忌讳的是静默放行遇到权限判断不生效先在has_permission里加日志确认resolve(request.path)的视图名和权限编码是否匹配。本文还有配套的精品资源点击获取
返回列表