Django 中的权限管理
今天我们会介绍一下 Django 中的自带的权限机制,并用一个案例帮助大家理解相关的权限用法。
1. Django 中自带的权限机制
当 Django 配置文件中的 INSTALL_APPS
包含了 django.contrib.auth
时,就默认启用了一个简单的权限系统,提供了为用户或组分配权限的方法。这个自带的权限系统是基于表的控制,权限最小粒度是表。也就是说,Django 的权限系统将控制某个用户或者用户组对某个模型表的权限,一旦赋予某个权限,将对表中的所有记录有效。
每个 Model 模型默认只有四个权限,分别对应着 add、change、delete 和 view。对应的权限表为 auth_permission,我们可以看看里面的内容:
该表只有四个字段,比较简单:id 为权限编号、name 为权限的描述、content_type_id 是关联字段,关联的是模型表、codename 是权限表示值,在校验权限的时候都是使用的这个值。来看看关联表 django_content_type 的内容:
可以看到 django_content_type 表中的 id 正是关联前面 auth_permission 表中的 content_type_id 字段,app_lable 表示应用名、model 表示对应应用下的模型表。
为了能演示 Django 的权限管理功能,我们在应用 hello_app 下新建一个告警消息模型 (alarm_message),并给这个模型显示定义两种权限:查看 (view)、删除 (delete),这样对于 alarm_message 会多出两个权限 。
class AlarmMessage(models.Model):
level_choices = (
(0, '警告'),
(1, '严重'),
(2, '危险'),
)
alarm_title = models.CharField('告警标题', max_length=30)
alarm_content = models.TextField('告警内容', default='暂无内容')
level = models.SmallIntegerField('高级级别', choices=level_choices, default=0)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return "<%d, %s>" % (self.id, self.title)
class Meta:
db_table = 'alarm_message'
# 通过设置这个会将默认生成的4种权限情况
# default_permissions = ()
permissions = (
('view_alarm_message', '查看告警消息'),
('delete_alarm_message', '删除告警消息'),
)
然后我们使用 makemigrations
和 migrate
命令来生成相关的模型表以及添加对应的权限信息到权限表中:
(django-manual) [root@server first_django_app]# python manage.py makemigrations
Migrations for 'hello_app':
hello_app/migrations/0010_alarmmessage.py
- Create model AlarmMessage
(django-manual) [root@server first_django_app]# python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, guardian, hello_app, sessions
Running migrations:
Applying hello_app.0010_alarmmessage... OK
我们来对应创建用户以及相应的告警信息,来对应的分配角色:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.models import User
>>> s1 = User(username="运维负责人", email="123@163.com")
>>> s1.set_password('master1234')
>>> s1.save()
>>> s2 = User(username="运维部员工", email="227@163.com")
>>> s2.set_password('develop1234')
>>> s2.save()
>>> s3 = User(username="用户", email="289555@qq.com")
>>> s3.set_password('user1234')
>>> s3.save()
这里我们定义了3个角色:平台负责人、平台员工以及普通用户,他们对告警信息表的权限分别如下:
- 平台负责人:查看 (view) 和 删除 (delete) 权限;
- 平台普通开发者:查看 (view) 权限;
- 外部用户:无权限
>>> User.objects.all().get(username="运维负责人").has_perm('hello_app.view_alarm_message')
False
>>> User.objects.all().get(username="运维负责人").has_perm('hello_app.delete_alarm_message')
False
>>> User.objects.all().get(username='运维负责人').user_permissions.add(126, 127)
>>> User.objects.all().get(username="运维负责人").has_perm('hello_app.view_alarm_message')
True
>>> User.objects.all().get(username="运维负责人").has_perm('hello_app.delete_alarm_message')
True
>>> User.objects.all().get(username="运维负责人").get_all_permissions()
{'hello_app.delete_alarm_message', 'hello_app.view_alarm_message'}
上面添加了运维负责人的查看和删除告警信息模型的权限。查看是否具有某个权限用 has_perm()
方法,参数是 应用.权限值
;添加权限用上面的 add()
方法,参数是权限表 auth_permission 中对应权限的 id。此外,对应的删除权限用 remove()
方法,设置权限用 set()
方法;获取用户的所有权限用 get_all_permissions()
方法。以下对应添加平台开发员工的权限:
>>> User.objects.all().get(username="运维部员工").user_permissions.add(126)
>>> User.objects.all().get(username="运维部员工").get_all_permissions()
{'hello_app.view_alarm_message'}
上面这些权限-用户信息被记录到表 auth_user_user_permissions 中,对应生成的结果如下:
在后端的 View 视图校验中,我们可以使用 has_perm()
方法来判断登录用户的权限。
def delete_alarm_message(request):
if not request.user.has_perm('hello_app.delete_alarm_message')
return HttpResponse('403 Forbidden')
此外 Django 还提供了一个permission_required()
的装饰器,可以快速的来校验用户是否拥有特定的权限,用法如下:
@permission_required(perm, login_url=None, raise_exception=False)
perm 参数为权限名称,同样是 应用.权限值
;login_url 参数值指的是是当没有权限时的跳转地址,没有设置则不会跳转;reise_exception 参数为 True 是表示当用户没有权限时,不会跳转,而是抛出 PermissionDenied
错误,返回 403 Forbidden
。
2. 案例
有了上面的基础之后,我们来完成一个简单的权限管理案例。首先添加3条告警记录,用于作为后面测试数据:
(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from hello_app.models import AlarmMessage
>>> m1 = AlarmMessage(alarm_title='服务异常告警', alarm_content='后台认证管理服务不存在,请检查', level=0)
>>> m1.save()
>>> m2 = AlarmMessage(alarm_title='内存告警', alarm_content='主机host-001内存使用率100%,请检查', level=1)
>>> m2.save()
>>> m3 = AlarmMessage(alarm_title='主机掉线', alarm_content='主机host-001已掉线,请检查', level=2)
>>> m3.save()
我们完成一个登录页面,同样是利用 session 保存登录状态。针对三个不同用户登录,我们会显示不同的结果:普通用户直接提示没有权限登录、平台员工登录后显示告警列表、平台负责人登录后除了显示告警列表外,每个记录对应有个删除按钮。
首先来看模板页面,主要有两个:登录页面 (test_form2.html) 和告警信息 (test_permission.html) 列表页面。
{# 代码位置: template/test_form2.html #}
{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="{% static 'css/main.css' %}" />
{% if not success %}
<form action="/hello/test_form_view2/" method="POST">
{% csrf_token %}
<div><span>{{ form.name.label }}:</span>{{ form.name }}
<div><span>{{ form.password.label }}:</span>{{ form.password }}
<div>
{{ form.save_login }}{{ form.save_login.label }}
</div>
<div><input class="input-text input-red" type="submit" value="登录" style="width: 214px"/></div>
{% if err_msg %}
<div><label class="color-red">{{ err_msg }}</label</div>
{% endif %}
</form>
{% else %}
<p>登录成功</p>
{% endif %}
{# 代码位置: template/test_permission.html #}
{% load staticfiles %}
<p>登录用户:{{ username }}
<a href="/hello/logout/" style="margin-left:30px">登出</a>
</p>
<div>
<table border="1" class="member-table">
<thead>
<tr>
<th></th>
<th>告警编号</th>
<th>告警标题</th>
<th>告警级别</th>
<th>告警内容</th>
<th>产生时间</th>
{% if perms.hello_app.delete_alarm_message %}
<th>操作</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for message in messages %}
<tr>
<td></td>
<td>{{ message.id }}</td>
<td>{{ message.alarm_title }}</td>
<td>{{ message.level }}</td>
<td>{{ message.alarm_content }}</td>
<td>{{ message.created_at|date:"Y-m-d H:m:s" }}</td>
{% if perms.hello_app.delete_alarm_message %}
<td><a href="/hello/alarm_delete/{{ message.id }}">删除</a></td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
<div >
<div class="page">
</div>
</div>
</div>
我们准备视图函数,这个视图函数是在前面的基础上进行了改造,代码如下:
# 代码位置:hello_app/views.py
# ...
class TestFormView2(TemplateView):
template_name = 'test_form2.html'
def get(self, request, *args, **kwargs):
# print('进入get()请求')
success = False
err_msg = ''
form = LoginForm()
if request.session.get('has_login', False):
logined_user = User.objects.all().get(id=int(request.session['user_id']))
# 判断登录的用户是否有权查看告警页面
if logined_user.has_perm('hello_app.view_alarm_message'):
# 这一步不能掉,request中添加用户信息
request.user = user
messages = AlarmMessage.objects.all()
return render(request, "test_permission.html", {"username":logined_user.username, "messages": messages})
else:
success = False
err_msg = "用户无权访问"
return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
def post(self, request, *args, **kwargs):
# print('进入post请求')
form = LoginForm(request.POST)
success = True
err_msg = ""
if form.is_valid():
login_data = form.clean()
name = login_data['name']
password = login_data['password']
# 登录认证
user = authenticate(username=name, password=password)
if not user:
success = False
err_msg = "用户名密码不正确"
# 判断登录用户是否有权限
elif user.has_perm('hello_app.view_alarm_message'):
request.user = user
request.session['has_login'] = True
request.session['user_id'] = user.id
# 设置100s后过期
request.session.set_expiry(100)
messages = AlarmMessage.objects.all()
return render(request, "test_permission.html", {"username": user.username, "messages": messages})
else:
success = False
err_msg = "用户无权访问"
else:
success = False
err_msg = form.errors['password'][0]
return self.render_to_response(context={'success': success, 'err_msg': err_msg, 'form': form})
def logout(request, *args, **kwargs):
if 'has_login' in request.session:
del request.session["has_login"]
if 'user_id' in request.session:
del request.session["user_id"]
request.session.flush()
return redirect('/hello/test_form_view2/')
我们这里除了改造原来的登录请求外,还多加了一个 logout()
方法用于用户登出操作。接下来准备好 URLConf 配置如下:
# 代码位置:hello_app/urls.py
# ...
urlpatterns = [
# ...
path('test_form_view2/', views.TestFormView2.as_view(), name='test_form_view2'),
path('logout/', views.logout, name='logout'),
]
最后,我们启动我们的工程,来看看实际的效果,具体演示演示如下:
3. 小结
本小节中,我们学习了 Django 中自带的权限管理机制,然后手动完成了一个简单的权限管理案例,以加深我们对 Django 内部权限机制的理解。但是这个权限管理的最小粒度是表,后面我们会使用 django-guardian 框架完成一个更加细粒度的权限管理案例。