Django内置权限系统源码解读

Django内置权限系统源码解读

前言

之前有篇文章 Django自定义认证系统原理及源码分析解读 带大家分析解读了Django的认证逻辑,而且我们也知道认证是基础,认证通过之后,用户登录到系统,能看到那些,能操作那些,这些都是有权限控制 的。

那么本篇就带领大家了解下 Django的权限系统

默认权限

Django在创建模型之后,默认会为每个模型提供增删改查 四个权限。默认的权限信息可以在auth_permission 表中查看到

sqlite> select * from auth_permission ;
1|1|add_logentry|Can add log entry
2|1|change_logentry|Can change log entry
3|1|delete_logentry|Can delete log entry
4|1|view_logentry|Can view log entry
5|2|add_permission|Can add permission
6|2|change_permission|Can change permission
7|2|delete_permission|Can delete permission
8|2|view_permission|Can view permission
9|3|add_group|Can add group
10|3|change_group|Can change group
11|3|delete_group|Can delete group
12|3|view_group|Can view group
13|4|add_user|Can add user
14|4|change_user|Can change user
15|4|delete_user|Can delete user
16|4|view_user|Can view user
... ...

这个默认权限是怎么创建的呢?

1、我们知道模型创建之后,最终需要通过 python manage.py migrate 在数据库中生效。

所以首先要看 migrate django命令的源码

而且如果是自定义Model对应的命令的话,也是在模型APP目录下创建 commands 同名目录,然后创建命令,且是以命令命名文件名称, 文件内容是 class Command(BaseCommand) 类的定义

# django/core/management/commands/migrate.py

from django.core.management.sql import (
    emit_post_migrate_signal, emit_pre_migrate_signal,
)

class Command(BaseCommand):
    help = "Updates database schema. Manages both apps with migrations and those without."
    requires_system_checks = []
    ... ...
    @no_translations
    def handle(self, *args, **options):
        database = options['database']
        ... ...
        emit_post_migrate_signal(
            self.verbosity, self.interactive, connection.alias, apps=post_migrate_apps, plan=plan,
        )

从这里得知,在migrate的最后调用了 emit_post_migrate_signal 信号

2、再查看emit_post_migrate_signal信号源码

# django/core/management/sql.py

def emit_post_migrate_signal(verbosity, interactive, db, **kwargs):
    # Emit the post_migrate signal for every application.
    for app_config in apps.get_app_configs():
        if app_config.models_module is None:
            continue
        if verbosity >= 2:
            print("Running post-migrate handlers for application %s" % app_config.label)
        models.signals.post_migrate.send(
            sender=app_config,
            app_config=app_config,
            verbosity=verbosity,
            interactive=interactive,
            using=db,
            **kwargs
        )

发现它调用了 models.signals.post_migrate 信号,如果了解过Django的信号机制,那么就知道 Django默认定义了一些内置模型相关的信号,都是在 django/db/models/signals.py

3、而Django默认的User、Permission模型等都是存在于 auth APP下,所以查看对应的源码

# django/contrib/auth/apps.py

from django.db.models.signals import post_migrate

class AuthConfig(AppConfig):
    default_auto_field = 'django.db.models.AutoField'
    name = 'django.contrib.auth'
    verbose_name = _("Authentication and Authorization")

    def ready(self):
        post_migrate.connect(
            create_permissions,
            dispatch_uid="django.contrib.auth.management.create_permissions"
        )
    ... ...

发现在auth apps.py 中就调用了 post_migrate 信号机制去调用create_permissions 函数

4、查看create_permissions 函数源码

# django/contrib/auth/management/__init__.py

from django.contrib.contenttypes.management import create_contenttypes


def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
    if not app_config.models_module:
        return

    # Ensure that contenttypes are created for this app. Needed if
    # 'django.contrib.auth' is in INSTALLED_APPS before
    # 'django.contrib.contenttypes'.
    create_contenttypes(app_config, verbosity=verbosity, interactive=interactive, using=using, apps=apps, **kwargs)

    app_label = app_config.label
    ... ...

发现在 create_permissions 函数,先会调用 create_contentypes 函数创建对应的 contenttype 然后在创建permissions 。

最终的结果就是看到在 auth_permissions 表中的记录,注意改变content_type 是关联到 auto_content_type 的,所以是先创建 content_type再创建permission

扩展

这里扩展简单说下Django的信号机制

Django的信号机制不同于Linux的信号机制,Django 中的信号用于在框架执行操作时解耦。当某些动作发生的时候,系统会根据信号定义的函数执行相应的操作

Django的信号主要包含以下三个要素:

  • 发送者(sender):信号的发出方。
  • 信号(signal):发送的信号本身。
  • 接收者(receiver):信号的接收者。

其中接受者就是回调函数,会把这个函数注册到信号之上。当特定事件发生之后,发送者发送信号,然后执行回调函数。

Django通过信号的connect() 函数监听发送者发送的信号,进行回调函数的处理。如果connect中没有明确指出具体的sender,那么它就监听所有sender

connect() 函数的源码

# django/dispatch/dispatcher.py

class Signal:
    ... ...
    def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
        """
        Connect receiver to sender for signal.
            sender
                The sender to which the receiver should respond. Must either be
                a Python object, or None to receive events from any sender.

核心关注 or None to receive events from any sender. 而 post_migrate 信号注册 create_permissions 回调函数的源代码在 django/contrib/auth/apps.py 具体详见上面第三步分析

相关的 django监听信号文档 官方文档参考这里, 另外个人还有另外整理的一篇关于 Django的信号机制解读 欢迎阅读

模型自定义权限

这里说的模型自定义权限,是指的Django内置的权限的自定义

权限定义很简单,主要是在模型的Meta属性中,配置 permissions 属性值,是一个列表,每个元素是一个二元组,

class Post(models.Model):
    name = models.CharField(max_length=32)
    def __str__(self):
        return self.name

    class Meta:
        permissions = [('can_export_posts', 'can export posts')]

然后执行了migrate之后,就会在系统的 auth_permission 表中新增一条记录

-- 模型默认的 增删改查4个权限
41|11|add_post|Can add post
42|11|change_post|Can change post
43|11|delete_post|Can delete post
44|11|view_post|Can view post
-- 新增的权限记录
45|11|can_export_posts|can export posts

1、判断用户权限

>>> from django.contrib.auth.models import User
>>> user = User.objects.filter(username='james').first()
# 获取用户的所有权限
>>> user.get_all_permissions()
{'demoapp.add_post', 'demoapp.view_post'}

# 查看用户都有哪些查看权限的方法 (输入 user.has_ 之后tab 键)
>>> user.has_
user.has_module_perms(     user.has_perm(             user.has_perms(            user.has_usable_password(

# 查看用户是否具有 某个权限
>>> user.has_perm('demoapp.add_post')
True
>>> user.has_perm('demoapp.delete_post')
False
    
# 这里了解 has_module_perms()的方法是为了后续讲解 admin后台权限按钮时做准备  
#     
>>> user.has_module_perms()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: has_module_perms() missing 1 required positional argument: 'app_label'
>>> user.has_module_perms('demoapp')
True

#  has_perms 检查用户是否具有一组权限,给定列表要的只要有一个没有权限就返回False   
>>> user.has_perms()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: has_perms() missing 1 required positional argument: 'perm_list'
>>> user.has_perms(['demoapp.add_post'])
True
>>> user.has_perms(['demoapp.add_post', 'demoapp.view_post'])
True
>>> user.has_perms(['demoapp.add_post', 'demoapp.view_post', 'demoapp.delete_post'])
False

2、新增用户权限

from django.contrib.auth.models import User, Permission

# 获取权限
add_post = Permission.objects.get(codename='add_post')
view_post = Permission.objects.get(codename='view_post')
change_post = Permission.objects.get(codename='change_post')

# 将user的权限设置为当前给定权限值,但是之前权限的会自动去掉
user.user_permission.set([add_post])

# 给用户 user 在当前的权限基础上新增权限
user.user_permission.add(view_post)
user.user_permission.add(view_post, change_post)

# 删除给定的权限
user.user_permission.remove(change_post)

# 清空所有权限
user.user_permission.clear()

3、用户权限组操作

阅读过Django的 auth.models 的源码就知道, Group 模型的permissions属性 关联Permission模型,而User模型有group属性

所以也可以通过 给 分配权限,然后给用户分配 来实现给用户一次性分配一组权限

具体的实操演示大家可以自行完成,类似与上面的直接给用户分配权限

需要额外说明的, user.get_all_permissions() 其实就是 user.get_user_permissions() + user.get_group_permissions() 的合集

# django/contrib/auth/models.py

class PermissionMixin(models.Model):
    ... ...
    def get_user_permissions(self, obj=None):
        return _user_get_permissions(self, obj, 'user')

    def get_group_permissions(self, obj=None):
        return _user_get_permissions(self, obj, 'group')

    def get_all_permissions(self, obj=None):
        return _user_get_permissions(self, obj, 'all')

扩展说明

扩展1、关于 has_module_perms 源码, 用户对象具有模型的任意一个权限,那么就返回True

# django/contrib/auth/backends.py

class ModelBackend(BaseBackend):
    ... ...
    def has_module_perms(self, user_obj, app_label):
        """
        Return True if user_obj has any permissions in the given app_label.
        """
        return user_obj.is_active and any(
            perm[:perm.index('.')] == app_label
            for perm in self.get_all_permissions(user_obj)
        )

扩展2、”权限“ 实质上只是一个"描述符", 告知你具有什么权限而已;然后根据”权限(描述符)“ 去判断用户能否去执行那些”动作(action)“

Django 自带的管理后台,我们知道需要管理自定义的APP种的模型的时候,需要”注册“ 对应的模型到 "admin" 中去

# demoapp/apps.py
from django.contrib import admin
from .models import Post

admin.site.register(Post)

这里的 admin 最终指向的是 AdminSite

1、首先根据 from django.contrib import admin 找到 django/crontrib/admin/__init__.py

def autodiscover():
    autodiscover_modules('admin', register_to=site)

2、然后看 admin.site 指的是什么

# django/contrib/admin/sites.py
site = DefaultAdminSite()

3、然后看这个 DefaultAdminSite

class DefaultAdminSite(LazyObject):
    def _setup(self):
        AdminSiteClass = import_string(apps.get_app_config('admin').default_site)
        self._wrapped = AdminSiteClass()

4、所以需要找 admin 的 default_site

# django/contrib/admin/apps.py

class SimpleAdminConfig(AppConfig):
    """Simple AppConfig which does not do automatic discovery."""

    default_auto_field = 'django.db.models.AutoField'
    default_site = 'django.contrib.admin.sites.AdminSite'
    name = 'django.contrib.admin'
    verbose_name = _("Administration")
    ... ...

所有最终我们要研究的是 AdminSite 类的源码

这里截取部分代码展示说明,判断权限

a)类中判断权限,设定相关URL地址

image.png

b) 然后在template的HTML页面中进行判断展示权限按钮

代码位于 django/contrib/admin/templates/admin/app_list.html

image.png

好了,今天的源码解读就到这里,如果有任何问题欢迎随时交流沟通,或者关于个人公众号 DailyJobOps

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 144,247评论 1 305
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,830评论 1 258
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,531评论 0 214
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,345评论 0 183
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,160评论 1 260
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,936评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,538评论 2 275
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,291评论 0 168
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,162评论 6 237
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,654评论 0 214
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,401评论 2 217
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,747评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,297评论 1 33
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,206评论 2 213
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,670评论 3 213
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,661评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,089评论 0 169
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,677评论 2 233
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,819评论 2 237

推荐阅读更多精彩内容