flyEn'blog

django中内置的权限控制

1. User Model

Django内置的权限系统包括以下三个部分:

  1. 用户(Users)
  2. 许可(Permissions):用来定义一个用户(user)是否能够做某项任务(task)
  3. 组(Groups):一种可以批量分配许可到多个用户的通用方式

首先需要在Django中安装这个组件:

  1. 将’django.contrib.auth’和’django.contrib.contenttypes’放到settings.py中的INSTALLED_APPS中。(使用contenttypes的原因是auth中的Permission模型依赖于contenttypes)
  2. 执行数据库迁移

‘django.contrib.auth.models’里User模型有两个多对多的属性:groups和user_permissions

【例】es = User.objects.create_user(‘esperyong’,’esperyong@gmail.com’,’123456’)
1.直接将一个列表赋值给该属性:
es.groups = [group_list]
es.user_permissions = [permission_list]
2.使用add方法将对象加入:
es.groups.add(group, group, …)
es.user_permissions.add(permission, permission, …)
3.使用remove方法将对象删除:
es.groups.remove(group, group, …)
es.user_permissions.remove(permission, permission, …)
4.使用clear方法将所有对象删除:
es.groups.clear()
es.user_permissions.clear()

User对象有以下几个属性:

  1. username:字符串类型。必填。30个字符以内。
  2. first_name:字符串类型。可选。30个字符以内。
  3. last_name:字符串类型。可选。30个字符以内。
  4. email:可选。
  5. password:明文密码的hash或者是某种元数据。
  6. is_staff:Boolean类型。用这个来判断是否用户可以登录进入admin site。
  7. is_active:Boolean类型。用来判断该用户是否是可用激活状态。在删除一个帐户的时候,可以选择将这个属性置为False,而不是真正删除。这样如果应用有外键引用到这个用户,外键就不会被破坏。
  8. is_superuser:Boolean类型。该属性用来表示该用户拥有所有的许可,而无需明确的赋予给他。
  9. last_login:datetime类型。最近一次登陆时间。
  10. date_joined:datetime类型。创建时间。

除了DjangoModel对象的通用方法之外,User对象有以下特有方法:

  1. is_anonymous():
    永远返回False.用来将User对象和AnonymousUser(未登录的匿名用户)对象作区分用的识别方法。通常,最好用is_authenticated()方法。
  2. is_authenticated():
    永远返回True。该方法不代表该用户有任何的许可,也不代表该用户是active的,而只是表明该用户提供了正确的username和password。
  3. get_full_name():
    返回一个字符串,是first_name和last_name中间加一个空格组成。
  4. set_password(raw_password):
    调用该方法时候传入一个明文密码,该方法会进行hash转换。该方法调用之后并不会保存User对象。
  5. check_password(raw_password)
    如果传入的明文密码是正确的返回True。该方法和set_password是一对,也会考虑hash转换。
  6. set_unusable_password():
    将用户设置为没有密码的状态。调用该方法后,check_password()方法将会永远返回false。但是如果,调用set_password()方法重新设置密码后,该方法将会失效,has_usable_password()也会返回True。
  7. has_usable_password():
    在调用set_unusable_password()方法之后,该方法返回False,正常情况下返回True。
  8. get_group_permissions(obj=None)
    返回该用户通过组所拥有的许可(字符串列表每一个代表一个许可)。obj如果指定,将会返回关于该对象的许可,而不是模型。
  9. get_all_permissions(obj=None):
    返回该用户所拥有的所有的许可,包括通过组的和通过用户赋予的许可。
  10. has_perm(perm,obj=None)
    如果用户有传入的perm,则返回True。perm可以是一个格式为:’.‘的字符串。如果User对象为inactive,该方法永远返回False。和前面一样,如果传入obj,则判断该用户对于这个对象是否有这个许可。
  11. has_perms(perm_list,obj=None):
    和has_perm一样,不同的地方是第一个参数是一个perm列表,只有用户拥有传入的每一个perm,返回值才是True。
  12. has_module_perms(package_name):
    传入的是Django app label,按照’.‘格式。当用户拥有该app label下面所有的perm时,返回值为True。如果用户为inactive,返回值永远为False。
  13. email_user(subject,message,from_email=None)
    发送一封邮件给这个用户,依靠的是该用户的email属性。如果from_email不提供的话,Django会使用settings中的DEFAULT_FROM_EMAIL发送。
  14. get_profile():
    返回一个和Site相关的profile对象,用来存储额外的用户信息。

User对象的UserManager

  • create_user(username,email=None,password=None)
  • make_random_password(length=10,allowed_chars=’abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789’):该方法返回一个给定长度和允许字符集的密码。其中默认的allowed_chars有一些字符没有,比如i,l等等。

2. User Profile

使用UserProfile存储用户的额外信息

1
2
3
4
5
6
7
8
class UserProfile(models.Model):
# 和User的一对一关系属性,该属性必填.
user = models.OneToOneField(User)

# 其他需要存储的属性
# User因为是Django提供的,如果想要在其上增加一些自己需要的字段和方法,不太好加入,因此UserProfile是达成这个目标的一个有利工具
accepted_eula = models.BooleanField()
favorite_animal = models.CharField(max_length=20, default="Dragons.")

在settings中声明一个变量
AUTH_PROFILE_MODULE = ‘accounts.UserProfile’

这样,User对象的get_profile()方法就会返回这个对象了。(UserProfile对象不会和User一起自动创建),因此要注册一个handler到User的post_save signal。

1
2
3
4
5
6
7
8
9
10
from django.db.models.signals import post_save

# 定义了UserProfile
# ...

def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)

post_save.connect(create_user_profile, sender=User)

3. Login Logout

Login

需要两个函数:authenticate(username,password)login(request,user),位于django.contrib.auth模块中

  1. authenticate(usernae,password)函数需要两个参数username,password,如果校验通过则返回User对象,如果校验不通过返回None。

  2. login接受两个参数,第一个是request对象,第二个是user对象。login方法使用SessionMiddleware将userID存入session当中。

    1
    2
    3
    4
    user = authenticate(username=username, password=password)
    if user is not None:
    if user.is_active:
    login(request, user)

check_password(password,encoded):第一个参数是明文密码,第二个是加密过的密码。
make_password(password[,salt,hashers]):根据给定的明文密码,salt,和Django支持的加密算法,返回一个加密的密码。
is_password_unable(encoded_password):判断是否给定字符串是一个hashed密码,有机会通过check_password()函数的校验。

上述三个方法位于模块django.contrib.auth.hashers:

Logout

login(request)

signals

django.contrib.auth.signals.user_logged_in
django.contrib.auth.signals.user_logged_out
三个参数:

  1. sender:user的class,如果是logout事件该值有可能是None如果用户根本就没有验证通过。
  2. request:HttpRequest对象
  3. user:user对象,如果是logout事件该值有可能是None如果用户根本就没有验证通过。

decorator

django.contrib.auth.decorators.login_required([redirect_field_name=REDIRECT_FIELD_NAME,login_url=None])
两个参数:

  1. redirect_field_name:默认值是next。用来定义登陆成功之后的跳回之前访问界面的url。
  2. login_url:默认值是settings.LOGIN_URL。用来指定登陆界面的url。如果不传入改参数,就需要确保settings.LOGIN_URL的值是正确设置的。

4. 许可(Permission) 和 用户组(Group)

许可(Permissions)

django 会为每个用户给新出现的Model增加add,change,delete这三个权限。可以用任何一个user对象执行user.hash_perm(‘school.add_studygroup’)。——(add/change/delete,app:school,model:StudyGroup),都会返回True。

自定义一些许可

  1. 在Model类的meta属性中添加permissions定义。
    1
    2
    3
    4
    5
    6
    7
    8
    class Discussion(models.Model):
    ...
    class Meta:
    permissions = (
    ("open_discussion", "Can create a discussion"),
    ("reply_discussion", "Can reply discussion"),
    ("close_discussion", "Can remove a discussion by setting its status as closed"),
    )

模型类:Discussion。可以创建几个权限来对这个模型的权限许可进行控制,控制某些人可以发起讨论、发起回复,关闭讨论。

  1. 执行数据库迁移,数据库中就有这三个许可了。
  2. 之后可以将上面的权限赋予给用户,方法两种:
    (1).通过某一个user的user_permissions属性:
    user.user_permissions.add(permission, permission, …)
    (2).通过user的一个组,然后通过Group的permissions属性:
    group.permissions.add(permission, permission, …)

如果要判断一个用户是否有发表讨论的权限:user.has_perm(‘school.open_discussion’)  

Permission 属性
所属模块:django.contrib.auth.models
属性:

  1. name:必填。小于50个字符。例如:’Can publish’(描述)。
  2. content_type:必填。一个指向django_content_type数据库表,对于每一个Django模型,在这个表里面都有一个记录对应。
  3. codename:必填。小于100个字符。例如:’can_publish’。

编程创建权限

1
2
3
4
5
6
7
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get(app_label='school', model='Discussion')
permission = Permission.objects.create(codename='can_publish',
name='Can Publish Discussions',
content_type=content_type)

在界面中使用许可
在模板代码中,有两个属性,是django给你提供好的,一个是user,一个是perms。

  1. user:
    user变量是一个User或者AnoymousUser对象。

    1
    2
    3
    4
    5
    {% if user.is_authenticated %}
    <p>Welcome, {{ user.username }}. Thanks for logging in.</p>
    {% else %}
    <p>Welcome, new user. Please log in.</p>
    {% endif %}
  2. perms:
    perms变量是一个django.contrib.auth.context_processors.PermWrapper对象,对当前用户的User.has_module_perms和User.has_perm进行了封装。
    比如判断当前用户是否拥有school应用下的所有权限,则使用
    判断当前用户是否拥有school应用下发表讨论的权限,则使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {% if perms.school %}
    <p>You have permission to do something in the school app.</p>
    {% if perms.school.publish_discussion %}
    <p>You can discussion!</p>
    {% endif %}
    {% if perms.school.reply_discussion %}
    <p>You can reply discussion!</p>
    {% endif %}
    {% else %}
    <p>You don't have permission to do anything in the school app.</p>
    {% endif %}

实现: settings 中的TEMPLATE_CONTEXT_PROCESSORS中定义的django.contrib.auth.context_processors.auth处理器。在django进入解析Template之前,首相要经过这一个个的context_processor。

用户组(Group)

其作用是在权限控制中可以批量对用户的许可进行分配。
将一个以后加入到一个Gruop中,该用户就拥有了所有该Group所分配的所有许可。

Group 属性

  1. name:必填。少于80个字符。
  2. permissions:多对多引用。和user的user_permissions属性类型相同。我们可以通过下面代码给该组赋予权限。
    1
    2
    3
    4
    group.permissions = [permission_list]
    group.permissions.add(permission, permission, ...)
    group.permissions.remove(permission, permission, ...)
    group.permissions.clear()

5. Authentication backends

django实现的这套permission体系,在底层被抽象为authentication backends。实际开发中可能需要对已有的权限系统进行扩展。
django中,所有的authentication backends,可以配置settings中一个 变量AUTHENTICATION_BACKENDS来做到,默认的设置:
AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',)

校验:
user:authenticate(self,username=None,password=None) 或者authenticate(self,token=None),get_user(self,user_id)
permission:get_group_permissions,get_all_permission,has_perm,has_module_perms

6. Django实现Object级别的权限控制-django-guardian

安装配置django-guardian:

  1. pip install django-guardian
  2. 在INSTALLED_APPS变量中加入guardian:

    1
    2
    3
    INSTALLED_APPS = (
    'guardian',
    )
  3. 添加一个backend到AUTHENTICATION_BACKENDS中,这样django才会具有对对象的权限控制:

    1
    2
    3
    4
    AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend', # django默认的backend
    'guardian.backends.ObjectPermissionBackend',
    )

在guardian中,还支持对匿名用户AnoymousUser的Object级别的权限控制。(比如说允许匿名用户发言的论坛或者blog)这需要在settings中加入:ANONYMOUS_USER_ID=-1
之后执行数据库迁移,系统将会创建一个User实例,叫做AnonymouseUser

设置和使用对象权限

guardian.shortcuts.assign(perm, user_or_group, obj=None),这个方法接受3个参数:

  1. perm
    这个参数是一个字符串,代表一个许可,格式为<app>.<perm_codename>或者<perm_codename>格式,但如果第三个参数为None,则必须为前者格式。
  2. user_or_group
    这个参数是一个User或者Group类型的对象。
  3. obj
    这个参数就是相关的对象。可省略,若省略则赋予Model权限。

【实例】
赋予模型级别的权限:

1
2
3
4
from guardian.shortcuts import assign
user = User.objects.create(username='zhangsan')
assign('app.view_task',user)
user.has_perm('app.view_task') >>>True

注意:一旦赋予模型级别的权限,那么所有该模型的对象级别的权限就都有了。

赋予对象级别的权限:

1
2
3
4
5
task = Task.objects.create(summary='Some job', content='')
assign('app.view_task', user, task)
user = User.objects.get(username='liuyong')#刷新缓存
user.has_perm('app.view_task',task) >>>True
user.has_perm('app.view_task') >>>False #模型级别的权限还没有

通过设置group来使用户具有相应的权限:
group = Group.objects.create(name=’employees’)
assign(‘change_task’, group, task)
user.groups.add(group)
user.has_perm(‘change_task’, task) >>>True

删除某个用户对某个对象的某种许可,这个函数的参数和assign相同。
guardian.shortcuts.remove_perm(perm,user_or_group=None, obj=None)

1
2
3
4
from guardian.shortcuts import remove_perm
remove_perm('change_site', user, site)
user = User.objects.get(username='joe') #刷新user对象缓存
user.has_perm('change_site', site) >>> False

Guardian在View中的使用
guardian.shortcuts.get_perms(user_or_group,obj):返回user/group对象对obj对象所有的权限。
guardian.shortcuts.get_objects_for_user(user,perms,klass=None,use_groups=True,any_perm=False):获得该用户下指定perm列表中的所有对象。

1
get_objects_for_user(user,'app.change_post')# 返回可编辑帖子

guadian.core.ObjectPermissionChecker:该方法是一个用来判断权限的包装器,针对user和group提供权限相关的访问方法,主要有has_perm(perm,obj)和get_perms(obj)两个方法。并且提供缓存机制,在多次查找权限的时候可以使用它。

1
2
3
4
5
6
7
8
9
10
>>> epser = User.objects.get(username='esper')
>>> site = Site.objects.get_current()
>>> from guardian.core import ObjectPermissionChecker
>>> checker = ObjectPermissionChecker(esper) # 我们也可以传入组group对象
>>> checker.has_perm('change_site', site)
True
>>> checker.has_perm('add_site', site) # 这次将不会产生数据库查询
False
>>> checker.get_perms(site)
[u'change_site']

【add】site = Site.objects.get_current()
等效于 current_site = Site.objects.get(id=settings.SITE_ID)
site模型管理器具备一个get_current()方法获得当前的站点域

使用view的decorator

1
2
3
4
5
6
7
8
>>> joe = User.objects.get(username='joe')
>>> foobars = Group.objects.create(name='foobars')
>>> from guardian.decorators import permission_required_or_403
>>> from django.http import HttpResponse
>>> @permission_required_or_403('auth.change_group',
>>> (Group, 'name', 'group_name'))
>>> def edit_group(request, group_name):
>>> return HttpResponse('some form')

只有拥有对name=foobars的Group对象拥有auth.change_group权限的用户,才能够执行这个view函数,否则返回的将是状态码为403的Response对象。

1
2
3
4
>>> request.user = joe
>>> joe.groups.add(foobars)
>>> edit_group(request, group_name='foobars')
<django.http.HttpResponse object at 0x102b8c8d0> # view方法得以顺利访问了

Guardian在模版中的使用
标签:get_obj_perms
需要加载guardian_tags标签库 :

1
{% load guardian_tags %}

标签格式为:

1
{% get_obj_perms user/group for obj as "context_var" %}

例子代码如下:

1
2
3
4
{% get_obj_perms request.user for flatpage as "flatpage_perms" %}
{% if "delete_flatpage" in flatpage_perms %}
<a href="/pages/delete?target={{ flatpage.url }}">Remove page</a>
{% endif %}

孤儿对象许可
当我们删除User和相关的Object的时候,我们一定要删除其相关的所有UserObjectPermission和GroupObjectPermission对象。
三个处理方法:

  1. 显式编码。
  2. 通过其提供的自定义django命令。

    1
    2
    $ python manage.py clean_orphan_obj_perms
    Removed 11 object permission entries with no targets
  3. 定期调用guardian.utils.clean_orphan_obj_perms(),该函数会返回删除的对象数目,在python中,可以使用celery定期调度这个任务。

最合理的解决办法,手动编码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models.signals import pre_delete
from guardian.models import UserObjectPermission
from guardian.models import GroupObjectPermission
from school.models import StudyGroup

def remove_obj_perms_connected_with_user(sender, instance, **kwargs):
filters = Q(content_type=ContentType.objects.get_for_model(instance),
object_pk=instance.pk)
UserObjectPermission.objects.filter(filters).delete()
GroupObjectPermission.objects.filter(filters).delete()

pre_delete.connect(remove_obj_perms_connected_with_user, sender=StudyGroup)

Fork me on GitHub