当前位置:   article > 正文

django:认证系统_django issuper

django issuper

文章目录

Django 认证系统处理验证和授权:

  • 验证检查用户是否是他们的用户
  • 授权决定已验证用户能做什么

这里的认证用于指代这两个任务。这两个任务都依赖于 User 模块。

具体来说这个认证系统负责处理用户账号、组、权限和基于 cookie 的用户会话。

实现时由以下部分组成:

  • 用户 User
  • 权限 Permission:二进制(是/否)标识指定用户是否可以执行特定任务
  • 用户组 Group:将标签和权限应用于多个用户的一般方法
  • 可配置的密码哈希化系统
  • 为登录用户或限制内容提供的表单和视图工具
  • 可插拔的后端系统

一,权限与授权

(一)User 对象

User 对象是认证系统的核心,它通常代表了能与网站进行交互的人员,并用于允许诸如限制访问、注册用户、将内容与创建者关联等功能。

实现时 Django 的认证框架中只使用内置的 User 类,因此,无论是超级管理员还是普通用户,都只是被设置了特殊属性集的 User 对象。

1,User 模型

User 对象模型的源码位于django\contrib\auth\models.py,具体继承关系如下:

class AbstractBaseUser(models.Model):
class PermissionsMixin(models.Model):
class AbstractUser(AbstractBaseUser, PermissionsMixin):
class User(AbstractUser):
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述
User 对象的字段、属性与方法请参考官方文档:User model

2,创建用户

(1)创建普通用户

创建普通用户最直接的方法是使用 create_user() 函数

>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')

# 此时,user是一个已经保存到数据库中的User对象,但可以继续更改其属性
>>> user.last_name = 'Lennon'
>>> user.save()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

多数情况下,会先自定义用户模型,然后配合 ModelForm 从页面收集经过验证的用户数据,最后调用表单实例的方法来创建用户。

  • 这套逻辑多用于实现用户注册功能:
model.py:
class MyUser(AbstractUser):
    qq = models.CharField('QQ号码', max_length=16)
    weChat = models.CharField('微信账号', max_length=100)
    mobile = models.CharField('手机号码', max_length=11)

    def __str__(self):
        return self.username


forms.py:
class MyUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm.Meta):
        model = MyUser
        fields = UserCreationForm.Meta.fields
        fields += ('email', 'mobile', 'weChat', 'qq')


views.py:
# 使用表单实现用户注册
def registerView(request):
    if request.method == 'POST':
        user = MyUserCreationForm(request.POST)
        if user.is_valid():
            user.save()
            tips = '注册成功'
        else:
            tips = '注册失败'
    user = MyUserCreationForm()
    return render(request, 'user.html', locals())


user.html:
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="utf-8">
    <title>用户注册</title>
    <link rel="stylesheet" href="https://unpkg.com/mobi.css/dist/mobi.min.css">
</head>
<body>
<div class="flex-center">
    <div class="container">
        <div class="flex-center">
            <div class="unit-1-2 unit-1-on-mobile">
                <h1>用户注册</h1>
                {% if tips %}
                    <div>{{ tips }}</div>
                {% endif %}
                <form class="form" action="" method="post">
                    {% csrf_token %}
                    <div>用户名:{{ user.username }}</div>
                    <div>邮 箱:{{ user.email }}</div>
                    <div>手机号:{{ user.mobile }}</div>
                    <div>Q Q 号:{{ user.qq }}</div>
                    <div>微信号:{{ user.weChat }}</div>
                    <div>密 码:{{ user.password1 }}</div>
                    <div>密码确认:{{ user.password2 }}</div>
                    <button type="submit" class="btn btn-primary btn-block">注 册</button>
                </form>
            </div>
        </div>
    </div>
</div>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66

在这里插入图片描述

  • 实际上的用户注册部分更加复杂:是否需要阅读相关条款,是否使用网页验证码,是否已存在相同童虎,是否需要用邮箱等进行二次验证,是否允许第三方账号登录等等。
(2)创建超级用户

创建超级用户最直接的方法是使用 createsuperuser 命令

$ python manage.py createsuperuser
Username (leave blank to use 'administrator'): joe 
Email address: joe@example.com
Password:
Password (again):
Superuser created successfully.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然也能使用 create_superuser() 函数来创建超级用户:

  • create_user() 相同,但将 is_staffis_superuser 设置为 True。
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_superuser('root', 'root@root.com', 'root')
>>> user
<User: root>
>>> user.save()
>>> user.is_superuser
True
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

超级用户可以在管理后台创建用户。

3,更改密码

Django 不会在用户模型里保存原始明文密码,而只会存储经过计算的哈希值,因此不要试图直接操作用户的密码,这就是创建用户需要辅助函数的原因。

修改用户密码最直接的方法是使用 changepassword 命令

$ python manage.py changepassword root
Changing password for user 'root'
Password:
Password (again):
Password changed successfully for user 'root'
  • 1
  • 2
  • 3
  • 4
  • 5

也可以使用 set_password() 方法

>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='john')
>>> user.set_password('new password')
>>> user.save()
  • 1
  • 2
  • 3
  • 4

超级用户可以在管理后台修改用户密码。

4,验证用户

可以调用 authenticate() 根据 username 和 password 这两个参数来验证用户名参是否一致:

  • 如果后端验证有效,则返回 django.contrib.auth.models.User 对象,否则返回 None 。
  • 如果后端验证有效,则 is_authenticated 属性返回 True 。
from django.contrib.auth import authenticate
user = authenticate(username='john', password='secret')
if user is not None:
    # A backend authenticated the credentials
else:
    # No backend authenticated the credentials
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

5,删除用户

删除用户最直接的方法是用超级用户登陆管理后台进行删除。

也能使用用户实例的 delete() 方法删除用户:

>>> from django.contrib.auth.models import User
>>> user = User.objects.get(username='john').delete()
>>> user 
(1, {'user.User': 1})
  • 1
  • 2
  • 3
  • 4

实际上,只要是通过编程的方式创建、修改、删除用户,出于安全考虑,都需要经过用户认证并拥有相关权限才能实现。

(二)Group 对象

django.contrib.auth.models.Group 与 Linux 系统中的用户组的概念相同:

  • 可以先将权限分配给这些用户组。然后将用户添加到不同的用户组中,组里的用户会自动拥有该组的权限。
  • 也可以将一些标签或扩展功能分配给这些用户组。然后将用户添加到不同的用户组中,组里的用户会自动实现用户分类。

1,Group 模型

User 通过从 PermissionsMixin 中继承的 groups 字段与 Group 产生多对多关联。它俩本身各自是一张数据表,然后两张表的多对多关系由 auth_user_groups 数据表维护。

  • Group 对象可以通过 user_set 反向查询用户组中的用户。
  • 用户与组总共有三张表维护。

在这里插入图片描述

2,Group 操作

Group 模型和其他 Django 模型 一样拥有标准的数据访问方法

# 创建组
group = Group.objects.create(name="test")
  • 1
  • 2

3,User 与 Group

group = Group.objects.get(name="test")
user = User.objects.get(username="admin")

# 添加用户到组
group.user_set.add(user)
# user.groups.add(group)
# user.groups.set([group_list])	# 批量设置

# 从组中删除用户
group.user_set.remove(user)
# user.groups.remove(group)

# 用户是否在组中
group.user_set.get(user)
# user.groups.get(group)

# 获取用户所在的所有组
user.groups.all()
# 获取组中所有用户
group.user_set.all()

# 用户退出所在的所有组
user.groups.clear()
# 组清空所有用户
group.user_set.clear()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

实际开发中,多将组用于给一批用户赋予权限,前提是组拥有这些权限。当然也能直接给用户指定权限。

(三)Permission 对象

1,Permission 模型

User 通过从 PermissionsMixin 中继承的 user_permissions 字段与 Permission 产生多对多关联。它俩本身各自是一张数据表,然后两张表的多对多关系由 auth_user_user_permissions 数据表维护。

  • Permission 对象可以通过 user_set 反向查询有该权限的用户。
  • 用户与权限总共有三张表维护。

Group 通过自身的的 permissions 字段与 Permission 产生多对多关联。它俩本身各自是一张数据表,然后两张表的多对多关系由 auth_group_permissions 数据表维护。

  • Permission 对象可以通过 group_set 反向有该权限的组。
  • 用户与权限总共有三张表维护。

自此,理清用户、用户组和权限之间的关系:
在这里插入图片描述

  • 三张数据表+三张关系维护表

在这里插入图片描述

2,Permission 操作

Permission 模型和其他 Django 模型 一样拥有标准的数据访问方法

from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

# 创建权限
content_type = ContentType.objects.get_for_model(MyModel)
permission = Permission.objects.create(
    codename='test_model',
    name='Can Test Model',
    content_type=content_type,
)

# 删除权限
permission.delete()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 这里是设置模型的权限,即设置对象级别的权限。

3,分配权限

(1)Permission 与 User
user = User.objects.get(username="user")
permission1 = Permission.objects.get(codename="permission1")
permission2 = Permission.objects.get(codename="permission2")

# 向用户添加权限
user.user_permissions.add(permission1, permission2, ...)
# user.user_permissions.set([permission_list])

# 判断用户是否有某项权限
user.has_perm('MyModel.codename')
# user.has_perms(permission1, permission2, ...)

# 获取用户拥有的一切权限
user.get_all_permissions()

# 获取用户直接拥有的用户权限
user.get_user_permissions()

# 获取用户间接拥有的组权限
user.get_group_permissions()

# 删除用于的权限
user.user_permissions.remove(permission1, permission2, ...)

# 清空用户所拥有的一切权限
myuser.user_permissions.clear()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
(2)Permission 与 Group
group = Group.objects.get(name="test")
permission1 = Permission.objects.get(codename="permission1")
permission2 = Permission.objects.get(codename="permission2")

# 向组添加权限
group.permissions.add(permission1, permission2, ...)

# 获取组的所有权限
group.permissions.all()

# 删除组的权限
group.permissions.remove(permission1, permission2, ...)

# 清空组所拥有的一切权限
group.permissions.clear()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

(四)权限机制

Django 内置了一个权限系统,它提供了为指定的用户和用户组分配权限的方法。

  • 不仅可以为每个对象类型设置权限,还可以为每个指定对象实例设置权限。

1,默认权限

INSTALLED_APPS 安装了 django.contrib.auth 时,权限系统将确保你的每个 Django 模型在被创建后有四个默认权限:添加add、修改change、删除delete和查看view。

  • 在执行数据迁移命令前触发 post_migrate 信号,信号调用相关的权限创建函数为模型创建这四个默认权限。

一般来说,add、change、delete 和 view 是最基础的四种权限。实际上,为了给模型创建其他的操作数据能力的权限时,会直接在模型的 Meta 中进行指定,这个放到后面的自定义权限来说。

但无论如何创建的权限,始终都需要分配到具体的用户或者组上。

2,权限缓存

Django会缓存每个用户对象,包括其权限user_permissions

  • 组也存在权限缓存。

当你在代码中手动改变一个用户的权限后,你必须重新获取该用户对象才能获取最新的权限。

比如:

from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404

from myapp.models import BlogPost

def user_gains_perms(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    # any permission check will cache the current set of permissions
    user.has_perm('myapp.change_blogpost')

    content_type = ContentType.objects.get_for_model(BlogPost)
    permission = Permission.objects.get(
        codename='change_blogpost',
        content_type=content_type,
    )
    user.user_permissions.add(permission)

    # Checking the cached permission set
    user.has_perm('myapp.change_blogpost')  # False

    # Request new instance of User
    # Be aware that user.refresh_from_db() won't clear the cache.
    user = get_object_or_404(User, pk=user_id)

    # Permission cache is repopulated from the database
    user.has_perm('myapp.change_blogpost')  # True

    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

二,身份验证

如果说授权系统是对模型对象设置并授予权限,那么验证系统则提供了许多可在视图中通用化使用的验证方式来简化身份验证和权限验证的逻辑。

尽管不提供一些常见的 web 验证系统的特性,但其中一些常见问题的解决方案已在第三方插件中实现,比如:

(一)验证后端

之所以无法将验证和授权完整分离,是因为 django 在实现这两项机制的时候,共用一套验证后端产生了必要的耦合。

验证后端是所有的验证与授权相关的逻辑的具体实现,但却是可插拔的。

1,默认的验证后端

Django 维护了一个验证后端列表来用于检查验证,具体使用哪些验证后端则需要AUTHENTICATION_BACKENDS 配置中指定。

  • 默认为:
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
  • 1
  • AUTHENTICATION_BACKENDS 的顺序很重要,所以如果同一个用户名和密码在多个后端都有效,Django 会在第一个正向匹配时停止处理。
  • 如果一个后端抛出 PermissionDenied 异常,则验证流程立马终止,Django 不会继续检查其后的后端。

ModelBackend这是 Django 默认使用的验证后端。

  • 继承自基类 BaseBackend,并实现了其中的方法:
    在这里插入图片描述

它使用由用户标识符和密码组成的凭证对 AUTH_USER_MODEL 配置指定的用户模型进行验证。

  • 对于 Django 的默认 User 模型来说,用户标识符是用户名
  • 对于自定义用户模型来说,它是 USERNAME_FIELD 指定的字段

在这里插入图片描述

请参考官方文档:ModelBackend

2,内置的验证后端

请参考官方文档:Available authentication backends

(二)请求中的验证

Django 使用会话中间件验证中间件将身份验证系统挂接到请求对象中,具体就是:

  • SessionMiddleware 通过请求管理 sessions
  • AuthenticationMiddleware 使用会话将用户和请求关联。

这两个中间件会在每次请求中都会提供 request.user 属性

  • 如果当前没有用户登录,这个属性将会被设置为 AnonymousUser ,否则将会被设置为 User 实例。
  • 可以进一步使用 is_authenticated 属性判断当前系统中是否用户已经登录。
if request.user.is_authenticated:
    # Do something for authenticated users.
    ...
else:
    # Do something for anonymous users.
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

1,用户如何登陆

用户登录实际上就是将指定的用户信息加入到当前会话中,这可以通过 login() 函数完成:

  • 接受 HttpRequest 对象和 User 对象。这个函数会通过 Django 的 session 框架将用户的 ID 保存在会话中。
  • 在匿名会话期间设置的任何数据也都会在用户登录后保留在会话中。
from django.contrib.auth import authenticate, login

def my_view(request):
    username = request.POST['username']
    password = request.POST['password']
    user = authenticate(request, username=username, password=password)
    if user is not None:
        login(request, user)
        # Redirect to a success page.
        ...
    else:
        # Return an 'invalid login' error message.
        ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 先验证系统数据中是否存在当前用户,通过后再登陆。

分析 login() 源码:

def login(request, user, backend=None):
    """
    在请求中保留用户 ID 和后端。这样用户就不必对每个请求重新进行身份验证。请注意,匿名会话期间设置的数据会在用户登录时保留。
    """
    session_auth_hash = ''
    if user is None:
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):
    	# 获取密码字段的 HMAC。
        session_auth_hash = user.get_session_auth_hash()

    if SESSION_KEY in request.session:
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key()

    try:
        backend = backend or user.backend
    except AttributeError:
        backends = _get_backends(return_tuples=True)
        if len(backends) == 1:
            _, backend = backends[0]
        else:
            raise ValueError(
                'You have multiple authentication backends configured and '
                'therefore must provide the `backend` argument or set the '
                '`backend` attribute on the user.'
            )
    else:
        if not isinstance(backend, str):
            raise TypeError('backend must be a dotted import path string (got %r).' % backend)

    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender=user.__class__, request=request, user=user)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

2,用户如何登出

如果已经通过 django.contrib.auth.login() 登录的用户可以在视图中使用 django.contrib.auth.logout()函数退出登录:

  • 调用logout()时,当前请求的会话数据将被完全清除,这是为了防止另一个人使用同一个 web 浏览器登录并访问前一个用户的会话数据。
from django.contrib.auth import logout

def logout_view(request):
    logout(request)
    # Redirect to a success page.
  • 1
  • 2
  • 3
  • 4
  • 5

分析 logput() 源码:

def logout(request):
    """
    从请求中删除经过身份验证的用户 ID 并刷新他们的会话数据。
    """
    user = getattr(request, 'user', None)
    if not getattr(user, 'is_authenticated', True):
        user = None
    # 在用户登出之前发送信号,这样接收器就有机会找出谁登出了。
    user_logged_out.send(sender=user.__class__, request=request, user=user)
    # 立即刷新会话数据。
    request.session.flush()
    if hasattr(request, 'user'):
        from django.contrib.auth.models import AnonymousUser
        request.user = AnonymousUser()
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3,限制对未登录用户的访问

(1)原始方式

限制访问页面最原始的办法就是检查 request.user.is_authenticated 并重定向到登录页面:

from django.conf import settings
from django.shortcuts import redirect

def my_view(request):
    if not request.user.is_authenticated:
        return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
    # ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

或者显示一个错误信息:

from django.shortcuts import render

def my_view(request):
    if not request.user.is_authenticated:
        return render(request, 'myapp/login_error.html')
    # ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
(2)login_required 装饰器装饰函数视图

作为快捷方式,你可以使用 login_required() 装饰器要求用户必须登录才能执行这个视图。

  • 如果用户没有登录,会重定向到指定的路径。
  • 不会检查用户的 is_active 标识状态,但默认的 AUTHENTICATION_BACKENDS 会拒绝非正常用户。

有两种方法指定登录重定向路径:

  1. 指定settings.LOGIN_URL配置:绝对路径或 URL 模式 name。
viewa:
from django.contrib.auth.decorators import login_required
@login_required
def my_view(request):
    ...


settings:
...
LOGIN_URL = "/accounts/login/"


urls:
from MyApp.views import my_view
...
	path('accounts/login/', my_view),
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  1. 传入login_url 参数:绝对路径。
from django.contrib.auth.decorators import login_required

@login_required(login_url='/accounts/login/')
def my_view(request):
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
(3)LoginRequiredMixin 扩展类视图

LoginRequiredMixin 实现和 login_required 相同的行为。

  • 这个 Mixin 应该在最先被继承。
  • 未经验证用户的所有请求都会被重定向到登录页面或者显示 HTTP 403 Forbidden 错误。
  • 同样不会检查用户的 is_active 标识状态,但默认的 AUTHENTICATION_BACKENDS 会拒绝非正常用户。

用户验证被通过的具体行为由 raise_exception 属性决定:

  • 为 True ,当条件不被满足的时候会引发 PermissionDenied 异常。
  • 为False (默认),匿名用户会被重定向至 login_url 属性指定的登录页面。
from django.contrib.auth.mixins import LoginRequiredMixin

class MyView(LoginRequiredMixin, View):
    login_url = '/login/'
  • 1
  • 2
  • 3
  • 4

4,限制对通过测试的登录用户的访问

为了能根据某些权限或者其他测试来限制一登陆用户的访问能力,可以在视图里使用一些装饰器直接对 request.user 进行测试。

例如检查用户是否拥有特定域名的邮箱:

from django.shortcuts import redirect

def my_view(request):
    if not request.user.email.endswith('@example.com'):
        return redirect('/login/?next=%s' % request.path)
    # ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
(1)user_passes_test

user_passes_test 中的调用返回 False 时执行重定向。

user_passes_test(test_func, login_url=None, redirect_field_name='next')装饰器参数:

  • test_func:必要参数。必须是一个带有django.contrib.auth.models.User 对象的调用。
  • login_url:指定用户没有通过测试时跳转的绝对地址;如果你没指定,默认是 settings.LOGIN_URL
from django.contrib.auth.decorators import user_passes_test

def email_check(user):
    return user.email.endswith('@example.com')

@user_passes_test(email_check, login_url='/login/')
def my_view(request):
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
(2)UserPassesTestMixin

UserPassesTestMixin扩展完成与 user_passes_test 相同的效果:

from django.contrib.auth.mixins import UserPassesTestMixin

class MyView(UserPassesTestMixin, View):
	login_url = None
	
    def test_func(self):
        return self.request.user.email.endswith('@example.com')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

5,对通过验证的用户的权限限制

检查用户是否拥有特定的权限是一个相对常见的任务。

(1)permission_required

permission_required(perm, login_url=None, raise_exception=False)装饰器要求用户必须具有某权限才能执行视图:

  • perm:权限名,格式为 app_label .permission_codename
  • login_url:无权限时所要重定向到的登陆位置。默认是settings.LOGIN_URL
  • raise_exception:无权限时是否引发 PermissionDenied 错误,提示 the 403 (HTTP Forbidden) view 而不是跳转到登录页面。
from django.contrib.auth.decorators import permission_required

@permission_required('polls.add_choice', login_url='/loginpage/')
def my_view(request):
    ...
  • 1
  • 2
  • 3
  • 4
  • 5

如果既想使用 raise_exception 引发错误,也想给用户重定向登录的机会,那需要合用两个装饰器:

from django.contrib.auth.decorators import login_required, permission_required

@login_required
@permission_required('polls.add_choice', raise_exception=True)
def my_view(request):
    ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
(2)PermissionRequiredMixin

PermissionRequiredMixin扩展完成与 permission_required 相同的效果:

from django.contrib.auth.mixins import PermissionRequiredMixin

class MyView(PermissionRequiredMixin, View):
    permission_required = 'polls.add_choice'
    # Or multiple of permissions:
    permission_required = ('polls.view_choice', 'polls.change_choice')
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

6,在基于类的视图中重定向未通过验证的请求

所有用于类视图限制访问的扩展都继承AccessMixin ,它被用来配置当访问被拒绝时的视图的基本行为。

1,login_url属性:默认是 None。get_login_url() 的缺省返回值。

2,permission_denied_message属性:默认是空字符串。是get_permission_denied_message() 的返回值。

3,redirect_field_name属性:get_redirect_field_name() 的缺省返回值。默认是 “next” 。

4,raise_exception属性:如果这个属性被设置为 True ,当条件不被满足的时候会引发 PermissionDenied 异常。如果是 False (默认),匿名用户会被重定向至登录页面。

5,get_login_url()方法:返回当用户没有通过测试时将被重定向的网址。如果已设置,将返回 login_url ,否则返回 settings.LOGIN_URL

6,get_permission_denied_message():当 raise_exception 为 True 时,这个方法可以控制传递给错误处理程序的错误信息,以便显示给用户。默认返回 permission_denied_message 属性。

7,get_redirect_field_name()方法:返回查询参数名,包含用户登录成功后重定向的 URL 。如果这个值设置为 None ,将不会添加查询参数。默认返回 redirect_field_name 属性。

8,handle_no_permission()方法:根据 raise_exception 的值,这个方法将会引发 PermissionDenied 异常或重定向用户至 login_url ,如果已设置,则可选地包含 redirect_field_name

7,用于身份验证的内置视图

尽管我们可以自定义用户登录、注销和密码管理的视图,但这些对于比较通用的验证逻辑,django 都提供了内置的验证类视图供直接使用。

  • 利用了内置的验证表单,但你也可以使用自己的表单。
  • 没有为验证视图提供默认模板。你可以为你打算使用的视图创建自己的模板。
(1)使用视图

在项目中可以使用不同方法来实现这些视图。最简单的方法就是在 URLconf 中包含 django.contrib.auth.urls 提供的 URL :

urlpatterns = [
    path('accounts/', include('django.contrib.auth.urls')),
]
  • 1
  • 2
  • 3

将包含以下 URL 模式:

accounts/login/ [name='login']
accounts/logout/ [name='logout']
accounts/password_change/ [name='password_change']
accounts/password_change/done/ [name='password_change_done']
accounts/password_reset/ [name='password_reset']
accounts/password_reset/done/ [name='password_reset_done']
accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
accounts/reset/done/ [name='password_reset_complete']
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

另一种方法是直接在 URL 中引用特定的验证视图:

from django.contrib.auth import views as auth_views

urlpatterns = [
    path('change-password/', auth_views.PasswordChangeView.as_view()),
]
  • 1
  • 2
  • 3
  • 4
  • 5

将能直接设置更改视图行为的可选参数:

urlpatterns = [
    path(
        'change-password/',
        auth_views.PasswordChangeView.as_view(template_name='change-password.html'),
    ),
]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

第三种方法,就是通过子类去方便地自定义这些视图。

(2)所有的验证视图

请参考官方文档:All authentication views

(三)在模板内验证数据

除了能在视图的使用过程中进行身份和权限验证外,在模板的使用过程中也能进行验证:

当模板渲染 RequestContext 对象时,当前登录用户(User 实例或 AnonymousUser 实例)被保存在模板变量 {{ user }} 中:

{% if user.is_authenticated %}
    <p>Welcome, {{ user.username }}. Thanks for logging in.</p>
{% else %}
    <p>Welcome, new user. Please log in.</p>
{% endif %}
  • 1
  • 2
  • 3
  • 4
  • 5

当前登录用户的权限保存在模板变量 {{ perms }} 中:

{% if perms.foo %}
    <p>You have permission to do something in the foo app.</p>
    {% if perms.foo.add_vote %}
        <p>You can vote!</p>
    {% endif %}
    {% if perms.foo.add_driving %}
        <p>You can drive!</p>
    {% endif %}
{% else %}
    <p>You don't have permission to do anything in the foo app.</p>
{% endif %}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 也可以通过 {% if in %} 语句来查找权限:
{% if 'foo' in perms %}
    {% if 'foo.add_vote' in perms %}
        <p>In lookup works, too.</p>
    {% endif %}
{% endif %}
  • 1
  • 2
  • 3
  • 4
  • 5

三,Django 中的自定义认证

尽管 django 的认证系统考虑很是周到,但我们还是经常需要自定义认证,包括自定义权限、自定义用户模型、自定义验证方式等等。

这里将简单回顾 django在认证中会做什么,然后开始自定义认证指南。

(一)其它验证资源

1,指定验证后端

可按照一定的顺序将可用的验证后端配置为名为 AUTHENTICATION_BACKENDS 的列表值。

当有人调用 django.contrib.auth.authenticate()时,django 会按顺序尝试对所有的认证后端进行认证。直到所有后端都尝试过,或者当某个后端抛出 PermissionDenied 异常时定制多有验证。

2,编写一个验证后端的必要项目

验证后端是一个类,要自定义验证后端,需要像默认的 ModelBackend 那样继承基类 BaseBackend,并实现了其中的方法:

最重要的就是两个必要的用于身份认证的方法:

  • get_user(user_id):接收一个 user_id ——可以是用户名、数据库 ID 等,总之必须是用户对象的主键——然后返回一个用户对象或 None。
  • authenticate(request, **credentials):采用 request 参数和 credentials 作为关键字参数。credentials 可以是相关的验证参数、也可以是一个令牌,总之 authenticate() 应该检查它所得到的凭证,如果凭证有效,则返回一个与这些凭证相匹配的用户对象。如果无效,则应返回 None:
from django.contrib.auth.backends import BaseBackend

class MyBackend(BaseBackend):
    def authenticate(self, request, username=None, password=None):
        # Check the username/password and return a user.
        ...


class MyBackend(BaseBackend):
    def authenticate(self, request, token=None):
        # Check the token and return a user.
        ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

由于 Django admin 与 Django User 对象紧密耦合,为了避免没有提供给 authenticate()reques 可能是 None 的情况,最好方法是为每个存在于后台的用户创建一个 Django User 对象(在你的 LDAP 目录中、外部 SQL 数据库中等等),你可以提前写一个脚本来完成这个任务,或者你的 authenticate 方法可以在用户第一次登录时完成。

下面是一个后端实例,它可以根据 settings.py 文件中定义的用户名和密码变量进行验证,并在用户第一次验证时创建一个 Django User 对象:

from django.conf import settings
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User

class SettingsBackend(BaseBackend):
    """
    根据 ADMIN_LOGIN 和 ADMIN_PASSWORD 设置进行身份验证。

    Use the login name and a hash of the password. For example:

    ADMIN_LOGIN = 'admin'
    ADMIN_PASSWORD = 'pbkdf2_sha256$30000$Vo0VlMnkR4Bk$qEvtdyZRWTcOsCnI/oQ7fVOu1XAURIZYoOZ3iq8Dr4M='
    """

    def authenticate(self, request, username=None, password=None):
        login_valid = (settings.ADMIN_LOGIN == username)
        pwd_valid = check_password(password, settings.ADMIN_PASSWORD)
        if login_valid and pwd_valid:
            try:
                user = User.objects.get(username=username)
            except User.DoesNotExist:
                # Create a new user. There's no need to set a password
                # because only the password from settings.py is checked.
                user = User(username=username)
                user.is_staff = True
                user.is_superuser = True
                user.save()
            return user
        return None

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

3,编写一个验证后端的可选项目

自定义的验证后端还可以选择性实现一组与权限相关的验证方法。

因为用户模型和它的管理器会把权限查找函数(get_user_permissions()get_group_permissions()get_all_permissions()has_perm()has_module_perms()with_perm())委托给任何实现了这些函数的认证后端。

所以,如果任何后端之一将一个权限赋予了用户,那么 Django 最终也将该权限赋予这个用户。也就是说,用户最终所拥有的权限将是所有验证后端所赋予用户的权限和。

这个多次赋予权限的流程,会在 has_perm()has_module_perms() 中引发 PermissionDenied 异常时被打断,进而导致验证失败。

后端可以像这样为管理员实现权限:

from django.contrib.auth.backends import BaseBackend

class MagicAdminBackend(BaseBackend):
    def has_perm(self, user_obj, perm, obj=None):
        return user_obj.username == settings.ADMIN_LOGIN
  • 1
  • 2
  • 3
  • 4
  • 5
(1)匿名用户的授权

匿名用户是指那些还没有验证过的用户,也就是说,他们还没有提供任何有效的验证信息。

然而,这并不一定意味着他们就无权做任何事。在最基本的层面上,大多数站点允许匿名用户浏览大部分页面,而且很多站点也允许匿名评论。

尽管 Django 的权限框架没有一个地方可以存储匿名用户的权限,但允许传递给验证后端的用户对象一个 django.contrib.uth.models.AnonymousUser 对象,从而允许后端为匿名用户指定自定义的授权行为。

这对于可重用应用的作者来说特别有用,他们可以将所有的授权问题委托给认证后端,而不是需要配置。

(2)非激活用户验证

非激活用户是指 is_active 字段设置为 False 的用户。

  • 官方建议通过将is_active 字段设置为 False 来注销用户账号而不是直接删除。

ModelBackendRemoteUserBackend 验证后端直接禁止这些用户进行验证。如果一个自定义用户模型没有 is_active 字段,那么所有用户都将被允许认证。

如果想让非激活用户能进行身份认证,可以使用 AllowAllUsersModelBackendAllowAllUsersRemoteUserBackend

权限系统中对匿名用户的支持,可以实现匿名用户有权限做某事而非激活的认证用户没有权限的场景。

  • 不要忘记在自己的后端权限方法中测试用户的 is_active 属性。
(3)处理对象权限

尽管 Django 的权限框架有一个对象权限的基础,但在核心中没有实现。这意味着对对象权限的检查总是返回 False 或一个空列表(取决于所执行的检查)。

  • 验证后端将为每个与对象相关的授权方法接收关键字参数 objuser_obj,并可以适当返回对象级别的权限。

Django自带的权限机制是针对模型的,这就意味着一个用户如果对 Article 模型有 change 的权限,那么该用户会获得可对所有文章对象进行修改的权限。如果我们希望实现对单个文章对象的权限管理,我们需要借助于第三方库比如django-guardian

(二)自定义权限

除了像前面那样单独创建 Permission 模型的数据来创建权限之外,更多的给模型创建权限的方法,就是使用模型 Meta 中的 permissions 属性来创建除默认权限之外的模型权限。举个例子

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/71660
推荐阅读
相关标签