Django 内置权限扩展:实现基于数据库实例的细粒度访问控制
本文详解如何在 Django 内置权限基础上,通过扩展 Model 添加 read_groups 和 write_groups 字段,实现针对具体数据库实例的细粒度读写权限控制。文章涵盖列表页过滤、查询接口鉴权及执行前权限校验三种场景,利用 ManyToMany 关系与 QuerySet 合并技巧,提供完整代码示例与实现思路,帮助开发者解决多部门数据库管理中的权限隔离问题。
当 Django 的内置权限机制无法满足业务需求时,开发者需要自行进行扩展设计。
背景介绍
背景介绍
overmind 项目采用了 Django 内置的权限系统,该系统基于 Model 层进行控制。每当创建新的 Model 时,Django 默认生成三个权限:add、change 和 delete。若给用户或用户组赋予 delete 权限,该用户即可删除该 Model 下的所有数据。
起初,overmind 仅管理本部门数据库,权限配置仅针对功能模块而非细粒度的数据库实例。例如,用户 A 拥有审核权限,即可审核所有数据库,此时内置权限系统足以满足需求。然而,随着系统完善并接入其他部门的数据库管理,需实现针对不同用户开放不同数据库的权限(如 A 部门用户仅能操作 A 部门数据库)。Django 基于 Model 的内置权限已无法直接满足此类需求。
权限扩展实现方案
实现过程
首先明确需求:
1. 保持原有的基于功能的权限控制不变,例如用户 A 拥有查询权限,用户 B 拥有审核权限。
2. 增加针对数据库实例的权限控制,例如用户 A 仅能查询特定的数据库,用户 B 仅能审核特定的数据库。
对于需求 1,Django 内置权限系统已能实现,此处不再赘述。重点在于需求 2:数据库信息存储在同一张表中,不同用户需操作不同的数据库记录。这意味着需要将每条数据库记录与有权限操作的用户进行关联。为便于管理,我们将数据库与用户组关联,即属于该用户组的用户均拥有权限。经分析,操作类型主要分为“读”和“写”两类,因此需为每个 MySQL 实例添加两个字段,分别记录拥有读权限和写权限的用户组。
在原有 Model 基础上添加 read_groups 和 write_groups 字段。数据库实例与用户组之间应为 ManyToManyField 多对多关系,即一个实例可关联多个用户组,一个用户组也可关联多个实例。
class Mysql(models.Model):
Env = (
(1, 'Dev'),
(2, 'Qa'),
(3, 'Prod'),
)
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')
project_id = models.IntegerField(verbose_name='项目')
project_tmp = models.CharField(max_length=128, default='')
environment = models.IntegerField(choices=Env, verbose_name='环境')
master_host = models.GenericIPAddressField(verbose_name='master主机')
master_port = models.IntegerField(default=3306, verbose_name='master端口')
slave_host = models.GenericIPAddressField(null=True, verbose_name='slave主机')
slave_port = models.IntegerField(null=True, default=3306, verbose_name='slave端口')
database = models.CharField(max_length=64, verbose_name='数据库')
read_groups = models.ManyToManyField(Group, related_name='read', verbose_name='读权限')
write_groups = models.ManyToManyField(Group, related_name='write', verbose_name='写权限')
description = models.TextField(null=True, verbose_name='备注')
Model 确定后,接下来分三部分详细介绍权限验证的具体实现。
列表页数据过滤
列表页权限控制

如上图所示,用户登录后进入系统,仅能查看其拥有读权限的 MySQL 实例列表,管理员则可查看全部实例。代码如下:
def mysql(request):
if request.method == 'GET':
if request.user.is_superuser:
_lists = Mysql.objects.all().order_by('id')
else:
# 获取登录用户的所有组
_user_groups = request.user.groups.all()
# 构造一个空的QuerySet然后合并
_lists = Mysql.objects.none()
for group in _user_groups:
_lists = _lists | group.read.all()
return render(request, 'overmind/mysql.index.html', {'request': request, 'lPage': _lists})
实现思路为:获取登录用户所属的所有用户组,循环查询每个组拥有读取权限的数据库实例,最后将各组有权限读取的数据库实例合并后返回。
获取登录用户的所有组使用了 ManyToMany 的查询方法:request.user.groups.all()
最终返回结果为一个 QuerySet,因此需先构造一个空的 QuerySet:Mysql.objects.none()
QuerySet 的合并不能简单使用相加运算符,而应使用:QuerySet-1 | QuerySet-2
查询接口鉴权逻辑
查询接口权限控制

如上图所示,系统中存在许多需根据项目、环境查询对应数据库信息的接口。对此类接口也需控制用户仅能查询其有权限读取的数据库实例,管理员可查看全部。代码如下:
def get_project_database(request, project, environment):
if request.method == 'GET':
_jsondata = {}
if request.user.is_superuser:
# 返回所有项目和环境匹配的DB
_lists = Mysql.objects.filter(
project_id=int(project),
environment=int(environment)
)
_jsondata = {i.id: i.database for i in _lists}
else:
# 只返回用户有权限查询的DB
_user_groups = request.user.groups.all()
for group in _user_groups:
# 循环mysql表中有read_groups权限的所有组
for mysql in group.read.all():
if mysql.project_id == int(project) and mysql.environment == int(environment):
_jsondata[mysql.id] = mysql.database
return JsonResponse(_jsondata)
实现思路与前文类似,但增加了根据项目和环境进行进一步判断的步骤。
需根据用户组反查包含该组的数据库实例,此处利用了 M2M 的 related_name 属性:group.read.all()
更多关于 Django ORM 查询的内容可参考文章《Django model select 的各种用法详解》,其中有详细总结。
执行操作前的权限校验
执行操作权限控制
除上述两个场景外,还需在执行具体操作前判断用户是否拥有权限。例如,在执行审核操作前,需判断用户对该数据库是否拥有写权限。
由于多处需进行此类判断,故将权限判断逻辑封装为独立方法处理,代码如下:
def check_permission(perm, mysql, user):
# 如果用户是超级管理员则有权限
if user.is_superuser:
return True
# 取出用户所属的所有组
_user_groups = user.groups.all()
# 取出Mysql对应权限的所有组
if perm == 'read':
_mysql_groups = mysql.read_groups.all()
if perm == 'write':
_mysql_groups = mysql.write_groups.all()
# 用户组和DB权限组取交集,有则表示有权限,否则没有权限
group_list = list(set(_user_groups).intersection(set(_mysql_groups)))
return False if len(group_list) == 0 else True
实现思路为:根据传入的第三个用户参数,获取该用户所属的所有用户组;再根据传入的第一个参数(读取或写入)和第二个参数(数据库实例),获取拥有相应权限的所有用户组;最后对两组取交集,若交集非空则表示有权限,否则无权限。
M2M 的 .all() 取出的结果是列表,两个列表取交集的方法为:list(set(list-A).intersection(set(list-B)))
在 View 中使用该逻辑十分简单,如下所示:
def query(request):
if request.method == 'POST':
postdata = request.body.decode('utf-8')
_host = get_object_or_404(Mysql, id=int(postdata.get('database')))
# 检查用户是否有DB的查询权限
if check_permission('read', _host, request.user) == False:
return JsonResponse({'state': 0, 'message': '当前用户没有查询此DB的权限'})
写在最后
1. Django 有基于对象的第三方权限管理模块 Django-guardian,本项目未使用的原因在于:一来权限需求并不复杂,自行实现较为便捷;二来个人倾向于非必要不引用过多第三方包,以免增加后续升级与维护负担。
2. 本方案与代码尚不完美,若有更优方案建议或更优雅的代码写法,欢迎交流。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













