django:认证系统

导读:本篇文章讲解 django:认证系统,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

文章目录

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):

在这里插入图片描述
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()

多数情况下,会先自定义用户模型,然后配合 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>

在这里插入图片描述

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

(2)创建超级用户

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

$ python manage.py createsuperuser
Username (leave blank to use 'administrator'): joe 
Email address: joe@example.com
Password:
Password (again):
Superuser created successfully.

当然也能使用 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

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

3,更改密码

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

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

$ python manage.py changepassword root
Changing password for user 'root'
Password:
Password (again):
Password changed successfully for user 'root'

也可以使用 set_password() 方法

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

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

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

5,删除用户

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

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

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

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

(二)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")

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()

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

(三)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()
  • 这里是设置模型的权限,即设置对象级别的权限。

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()

(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()

(四)权限机制

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

    ...

二,身份验证

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

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

(一)验证后端

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

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

1,默认的验证后端

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

  • 默认为:
AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend']
  • 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,用户如何登陆

用户登录实际上就是将指定的用户信息加入到当前会话中,这可以通过 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.
        ...
  • 先验证系统数据中是否存在当前用户,通过后再登陆。

分析 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)

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.

分析 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()

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))
    # ...

或者显示一个错误信息:

from django.shortcuts import render

def my_view(request):
    if not request.user.is_authenticated:
        return render(request, 'myapp/login_error.html')
    # ...

(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. 传入login_url 参数:绝对路径。
from django.contrib.auth.decorators import login_required

@login_required(login_url='/accounts/login/')
def my_view(request):
    ...

(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/'

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)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):
    ...

(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')

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):
    ...

如果既想使用 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):
    ...

(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')

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')),
]

将包含以下 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']

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

from django.contrib.auth import views as auth_views

urlpatterns = [
    path('change-password/', auth_views.PasswordChangeView.as_view()),
]

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

urlpatterns = [
    path(
        'change-password/',
        auth_views.PasswordChangeView.as_view(template_name='change-password.html'),
    ),
]

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

(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 %}

当前登录用户的权限保存在模板变量 {{ 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 %}
  • 也可以通过 {% if in %} 语句来查找权限:
{% if 'foo' in perms %}
    {% if 'foo.add_vote' in perms %}
        <p>In lookup works, too.</p>
    {% endif %}
{% endif %}

三,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.
        ...

由于 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

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)匿名用户的授权

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

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

尽管 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 属性来创建除默认权限之外的模型权限。举个例子🌰:

app.models.py:
class Task(models.Model):
    ...
    class Meta:
        permissions = [
            ("change_task_status", "Can change the status of tasks"),
            ("close_task", "Can remove a task by setting its status as closed"),
        ]

然后只需要在使用这个模型时判断用户是否有某项权限:

user.has_perm('app.close_task')

(三)扩展现有的 User 模型

前面说了那么多,整个 django 认证系统的核心对象都是内置的 User 模型。尽管 User 模型提供了许多有用的字段,但还是可能满足不了关于用户模型的需求,此时就需要合理地扩展 User 模型。

有两种方法可以扩展默认的 User 模型

1,Profile 模式

不需要改变已有的 User 表结构,且如果AbstractUser提供的方法已经够用,仅需在已有表基础上添加一个用户额外的非认证相关信息,则可以将这些额外字段所在的表通过一对一外键与 User 模型关联起来。举个例子🌰:

from django.contrib.auth.models import User

class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    department = models.CharField(max_length=100)

假设现有的雇员 Fred Smith 同时拥有 User 和 Employee 模型,可以使用 Django 的标准关联模型约束来访问相关信息:

>>> user = User.objects.get(username='fsmith')
>>> freds_department = user.employee.department

如果要将 Profile 模式的字段添加到后台管理的用户页面中,则需要在应用程序的 admin.py 中定义一个 InlineModelAdmin ,并将其添加到一个 UserAdmin 类中,最后关联注册到管理后台。举个例子🌰:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User

from my_user_profile_app.models import Employee

# Define an inline admin descriptor for Employee model
# which acts a bit like a singleton
class EmployeeInline(admin.StackedInline):
    model = Employee
    can_delete = False
    verbose_name_plural = 'employee'

# Define a new User admin
class UserAdmin(BaseUserAdmin):
    inlines = (EmployeeInline,)

# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

这种模型的好处就是即插即用,但使用时会增加额外的外键查询。

2, Proxy 模式

如果不希望使用外键关联,可选择使用 Proxy 模式实现多表继承效果。

因为子模型需要存储父模型中不存在的额外字段,所以每个子类模型都会创建一张新表来包含父表数据和额外数据。不过,有时候你只想修改模型的 Python 级行为——可能是修改默认管理器,或添加一个方法。

这是代理模型继承的目的:为原模型创建一个代理。你可以创建,删除和更新代理模型的实例,所有的数据都会存储的像你使用原模型(未代理的)一样。不同点是你可以修改代理默认的模型排序和默认管理器,而不需要修改原模型。

代理模式只需像普通模型一样定义,通过将 Meta 类的 proxy 属性设置为 True。举个例子🌰:

from django.db import models

class Person(models.Model):
    first_name = models.CharField(max_length=30)
    last_name = models.CharField(max_length=30)

class MyPerson(Person):
    class Meta:
        proxy = True

    def do_something(self):
        # ...
        pass

MyPerson 类与父类 Person 操作同一张数据表。特别提醒, Person 的实例能通过 MyPerson 访问,反之亦然:

>>> p = Person.objects.create(first_name="foobar")
>>> MyPerson.objects.get(first_name="foobar")
<MyPerson: foobar>

也可以用代理模式定义模型的另一种不同的默认排序方法。你也许不期望总对 “Persion” 进行排序,但是在使用代理时,却希望依据 “last_name” 属性进行排序:

class OrderedPerson(Person):
    class Meta:
        ordering = ["last_name"]
        proxy = True

现在,普通的 Person 查询结果不会被排序,但 OrderdPerson 查询会按 last_name 排序。

(四)用自定义用户模型替换 User 模型

扩展现有的 User 模型通常是因为需要扩展的那些数据并不需要参与认证。但有些项目的这些数据可能会有认证需求,因而 Django 内置的 User 模型并不总是合适。例如,在一些网站上,使用电子邮件地址作为你的身份识别标记比使用用户名更有意义。

Django 要求必须通过为 AUTH_USER_MODEL 配置提供一个引用自定义模型的值来覆盖默认的 User 模型:

AUTH_USER_MODEL = 'myapp.MyUser'

1,启动项目时使用自定义的用户模型

官方强烈推荐为一个新的项目设置一个自定义的用户模型:

from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass

这个模型的行为与默认 User 模型一致(默认 User 模型就继承 AbstractUser 模型,但却没有做任何更多的自定义),但是能在未来需要的时候有够多的自定义权力。

这样自定义一个能与默认 User 模型一致行为的模型后,必须记得两件事:

  1. AUTH_USER_MODEL 指向它。
  2. 在 app 中的 admin.py 中注册它。

2,在项目中更改为自定义的用户模型

由于 Django 对可交换模型的动态依赖特性的限制,AUTH_USER_MODEL 所引用的模型必须在其应用的第一次迁移中创建,否则,在已经建立数据用户库表之后再去修改 AUTH_USER_MODEL
就会出现依赖问题:影响外键和多对多关系。

这个改动并不能自动完成,需要手动修复你的架构,将数据从旧的用户表移出,并有可能需要手动执行一些迁移操作,具体请查看步骤概述:如何从内置用户模型迁移到自定义用户模型

3,引用用户模型

可重用应用不应该实现自定义用户模型。
因为一个项目可能会使用很多应用,所以实现了自定义用户模型的可重用应用会产生用户模型引用冲突。

如果确实需要在应用中存储各自的用户的信息,可以使用一个 ForeignKeyOneToOneFieldAUTH_USER_MODEL:如果指定了自定义用户模型,则返回自定义用户模型,否则返回 User 模型——实现对用户模型的引用。举个例子🌰:

from django.conf import settings
from django.db import models

class Article(models.Model):
    author = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
    )

使用 django.contrib.auth.get_user_model() 也实现对当前活动的用户模型的引用。举个例子🌰:

from django.conf import settings
from django.db import models
from django.contrib.auth import get_user_model

class Article(models.Model):
    author = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE,
    )

总之,处于不同的考虑:有三种方法实现自己的用户模型:

  • Profile 模式扩展默认User模型
  • Proxy 模式扩展默认User模型
  • 自定义用户模型代替默认User模型

4,自定义用户、内置的表单

说实话,为使用默认的 User 模型的用户提供一些与认证相关的表单的工作是相当重复的体力劳动,因此 django 提供了一些内置的表单视图来完成数据收集和处理工作。

看一个内置的表单的源码就知道了:

UserCreationForm源码: django\contrib\auth\forms.py

class UserCreationForm(forms.ModelForm):
    """
    用于根据给定的用户名和密码创建没有特权的用户的表单。
    """
    error_messages = {
        'password_mismatch': _('The two password fields didn’t match.'),
    }
    password1 = forms.CharField(
        label=_("Password"),
        strip=False,
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        help_text=password_validation.password_validators_help_text_html(),
    )
    password2 = forms.CharField(
        label=_("Password confirmation"),
        widget=forms.PasswordInput(attrs={'autocomplete': 'new-password'}),
        strip=False,
        help_text=_("Enter the same password as before, for verification."),
    )

    class Meta:
        model = User
        fields = ("username",)
        field_classes = {'username': UsernameField}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self._meta.model.USERNAME_FIELD in self.fields:
            self.fields[self._meta.model.USERNAME_FIELD].widget.attrs['autofocus'] = True

    def clean_password2(self):
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError(
                self.error_messages['password_mismatch'],
                code='password_mismatch',
            )
        return password2

    def _post_clean(self):
        super()._post_clean()
        # Validate the password after self.instance is updated with form data
        # by super().
        password = self.cleaned_data.get('password2')
        if password:
            try:
                password_validation.validate_password(password, self.instance)
            except ValidationError as error:
                self.add_error('password2', error)

    def save(self, commit=True):
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user

具体请参考Custom users and the built-in auth forms

5,自定义用户和 django.contrib.admin

如果希望自定义的用户模型也与管理后台一起使用,那么这个用户模型必须定义一些额外的属性和方法来允许管理员控制用户对管理后台内容的访问,请参考CustomUser

还需要在 admin 文件里注册自定义的用户模型。
如果自定义的用户模型扩展了 django.contrib.auth.models.AbstractUser ,则可以直接使用Django已有的 django.contrib.auth.admin.UserAdmin 进行注册。
如果自定义的用户模型扩展了 AbstractBaseUser ,则需要自定义一个 ModelAdmin
不管怎样,你都将需要重写任何引用 django.contrib.auth.models.AbstractUser 上的字段的定义,这些字段不在你自定义的用户类中。

6,自定义用户和权限

为了便于将Django的权限框架引入到你自己的用户类中,Django提供了 PermissionsMixin ,请参考PermissionsMixin

这是一个抽象模型,可以包含在用户模型类层次结构中,为你提供支持Django权限模型所需的所有方法和数据库字段。

7,一个完整的例子

models.py:

from django.db import models
from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser
)


class MyUserManager(BaseUserManager):
    def create_user(self, email, date_of_birth, password=None):
        """
        Creates and saves a User with the given email, date of
        birth and password.
        """
        if not email:
            raise ValueError('Users must have an email address')

        user = self.model(
            email=self.normalize_email(email),
            date_of_birth=date_of_birth,
        )

        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, date_of_birth, password=None):
        """
        Creates and saves a superuser with the given email, date of
        birth and password.
        """
        user = self.create_user(
            email,
            password=password,
            date_of_birth=date_of_birth,
        )
        user.is_admin = True
        user.save(using=self._db)
        return user


class MyUser(AbstractBaseUser):
    email = models.EmailField(
        verbose_name='email address',
        max_length=255,
        unique=True,
    )
    date_of_birth = models.DateField()
    is_active = models.BooleanField(default=True)
    is_admin = models.BooleanField(default=False)

    objects = MyUserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = ['date_of_birth']

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        return True

    def has_module_perms(self, app_label):
        "Does the user have permissions to view the app `app_label`?"
        # Simplest possible answer: Yes, always
        return True

    @property
    def is_staff(self):
        "Is the user a member of staff?"
        # Simplest possible answer: All admins are staff
        return self.is_admin

admin.py:

from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError

from customauth.models import MyUser


class UserCreationForm(forms.ModelForm):
    """A form for creating new users. Includes all the required
    fields, plus a repeated password."""
    password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
    password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)

    class Meta:
        model = MyUser
        fields = ('email', 'date_of_birth')

    def clean_password2(self):
        # Check that the two password entries match
        password1 = self.cleaned_data.get("password1")
        password2 = self.cleaned_data.get("password2")
        if password1 and password2 and password1 != password2:
            raise ValidationError("Passwords don't match")
        return password2

    def save(self, commit=True):
        # Save the provided password in hashed format
        user = super().save(commit=False)
        user.set_password(self.cleaned_data["password1"])
        if commit:
            user.save()
        return user


class UserChangeForm(forms.ModelForm):
    """A form for updating users. Includes all the fields on
    the user, but replaces the password field with admin's
    disabled password hash display field.
    """
    password = ReadOnlyPasswordHashField()

    class Meta:
        model = MyUser
        fields = ('email', 'password', 'date_of_birth', 'is_active', 'is_admin')


class UserAdmin(BaseUserAdmin):
    # The forms to add and change user instances
    form = UserChangeForm
    add_form = UserCreationForm

    # The fields to be used in displaying the User model.
    # These override the definitions on the base UserAdmin
    # that reference specific fields on auth.User.
    list_display = ('email', 'date_of_birth', 'is_admin')
    list_filter = ('is_admin',)
    fieldsets = (
        (None, {'fields': ('email', 'password')}),
        ('Personal info', {'fields': ('date_of_birth',)}),
        ('Permissions', {'fields': ('is_admin',)}),
    )
    # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
    # overrides get_fieldsets to use this attribute when creating a user.
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('email', 'date_of_birth', 'password1', 'password2'),
        }),
    )
    search_fields = ('email',)
    ordering = ('email',)
    filter_horizontal = ()


# Now register the new UserAdmin...
admin.site.register(MyUser, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)

settings.py:

AUTH_USER_MODEL = 'customauth.MyUser'

四,密码管理

Django 努力提供了一个安全且灵活的管理用户密码的工具。密码管理这部分通常不应该被重新再设计。

(一)Django 如何存储密码

默认情况下,Django 使用带有 SHA256 哈希的 PBKDF2 算法,它是 NIST 推荐的密码延展机制。它足够安全,需要大量的运算时间才能破解,这对大部分用户来说足够了。
但也可以根据需求选择不同的算法,甚至使用自定义的算法来匹配特定的安全场景。

Django 通过查阅 PASSWORD_HASHERS 配置来选择算法。这是一个 Django 支持的哈希算法类列表,默认值是:

PASSWORD_HASHERS = [
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.Argon2PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.ScryptPasswordHasher',
]

实际上只有第一个条目( settings.PASSWORD_HASHERS[0] )将被用来存储密码,其他条目都是有效的哈希函数,可用来检测已存密码。
如果想使用不同算法,你需要修改settings.PASSWORD_HASHERS[0]

使用其他加密方式,请参考:在 Django 中使用 Argon2在 Django 中使用 bcrypt在 Django 中使用 scrypt

(二)手动管理用户的密码

django.contrib.auth.hashers模块提供了一组函数来创建和验证经过哈希处理的密码,可以独立于User模型而直接使用。

比较常用的是:

  • make_password函数:通过系统默认的加密算法将纯文本密码转换为用于数据库存储的哈希密码。

(三)新密码验证

Django 提供可插拔的密码验证器来避免用户选择弱密码。

  • 默认情况下,验证器在重置或修改密码的表单中使用,也可以在 createsuperuserchangepassword 命令中使用。
  • 密码通过所有的验证器并不能意味着它就是强密码。
  • 可以同时配置多个密码验证。

Django 默认已经在AUTH_PASSWORD_VALIDATORS 配置中包含了一些验证器:

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]

也可以编写你自己的验证,请参考Writing your own validator

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98059.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!