1-Django - auth组件

about

Django3.2 + Python3.9

Django通过内置auth模块实现了用户认证系统。

auth模块本身也是一个Django app,默认使用auth_user表来存储用户数据,对应的模型类是User。

本篇就来学习一下这个auth app为我们提供了哪些便利的用户认证功能。

快速上手

在你的Django项目中,无需写任何模型类。

在执行完数据库迁移命令之后,创建一个超级管理员:

(base) D:\mydata\MyProject\about_auth>python manage.py makemigrations
(base) D:\mydata\MyProject\about_auth>python manage.py migrate
(base) D:\mydata\MyProject\about_auth>python manage.py createsuperuser
Username (leave blank to use 'zhangkai'): root
Email address: root@qq.com
Password: root1234
Password (again): root1234
Superuser created successfully.

这样就有了一个超级管理员root密码root1234,用这个超级用户来登录admin后台,你也可以用这个超级用户来使用auth app提供的各种认证功能。

auth_user表和User模型类

上一小节中,一顿操作创建出来一个超级用户,那么这个超级用户被保存到哪张表里了?这个表有哪些字段需要我们关注.....

首先auth_user表其本质上对应就是auth app下models.py中的User模型类。

User模型类内部没干什么,主要功能全部继承于AbstractUser类。

from django.contrib.auth.models import User  # 这么导入
from django.contrib.auth.models import AnonymousUser  # 还有这个匿名对象的模型类也需要注意
# 长这样
class User(AbstractUser):
    """
    Users within the Django authentication system are represented by this
    model.

    Username and password are required. Other fields are optional.
    """
    class Meta(AbstractUser.Meta):
        swappable = 'AUTH_USER_MODEL'

可以看到User这个模型类,基本上没干啥活,字段映射关系都有父类AbstractUser干了.....

首先来说auth_user表结构长这样:

create table auth_user
(
	id integer not null
		primary key autoincrement,
	password varchar(128) not null,
	last_login datetime,
	is_superuser bool not null, -- 是否是超级用户
	username varchar(150) not null
		unique,
	last_name varchar(150) not null,  -- 英文 姓
	email varchar(254) not null,
	is_staff bool not null,  -- True表示能登录django admin后台,False表示不能登录django admin后台
	is_active bool not null, -- True表示该账户是激活的,可以在auth中进行登录认证,False表示未激活,可以在不删除用户的情况禁止登录
	date_joined datetime not null,
	first_name varchar(150) not null  -- 英文 名
);

其次要了解其模型类的继承关系。

简单介绍下:

  • UserManger类,主要提供创建用户信息这些操作。

  • User类,就是auth app默认的模型类。

  • AbstractUser类,User类的父类,主要实现了各种字段映射和其它功能,如权限,看看源码就知道了,解释的很清楚。

  • AbstractBaseUser类,是AbstractUser类的父类,扩展了AbstractUser类的功能,比如user对象是否通过认证?是否是匿名用户?设置密码、校验密码.....

  • PermissionsMixin类,是AbstractUser类的父类,扩展了AbstractUser类的功能,比如user对象是否是超级用户。

  • AnonymousUser类,如果用户认证失败,或者未认证,那么request.user就是AnonymousUser,它是AnonymousUser类的实例化对象。

    def get_user(request):
        """
        Return the user model instance associated with the given request session.
        If no user is retrieved, return an instance of `AnonymousUser`.
        """
        from .models import AnonymousUser
        user = None
        try:
            user_id = _get_user_session_key(request)
            backend_path = request.session[BACKEND_SESSION_KEY]
        except KeyError:
            pass
        else:
            if backend_path in settings.AUTHENTICATION_BACKENDS:
                backend = load_backend(backend_path)
                user = backend.get_user(user_id)
                # Verify the session
                if hasattr(user, 'get_session_auth_hash'):
                    session_hash = request.session.get(HASH_SESSION_KEY)
                    session_hash_verified = session_hash and constant_time_compare(
                        session_hash,
                        user.get_session_auth_hash()
                    )
                    if not session_hash_verified:
                        if not (
                            session_hash and
                            hasattr(user, '_legacy_get_session_auth_hash') and
                            constant_time_compare(session_hash, user._legacy_get_session_auth_hash())
                        ):
                            request.session.flush()
                            user = None
    
        return user or AnonymousUser()
    
  • 想要扩展auth_user表,我们就需要在自己的app的models.py中定义自己的模型类,来继承AbstractUser类即可。注意这只是做额外的扩展字段,比如添加一个手机号字段,但都可以使用auth app的各种功能。

  • 如果想要进一步扩展,那么我们就在自己的app的models.py中重写AbstractUser方法,只要也继承AbstractBaseUser和PermissionsMixin,也照样可以使用auth app的各种功能。

auth app的认证组件

再来介绍一下auth app提供的各种认证功能组件。

完整的项目是示例代码。
链接:https://pan.baidu.com/s/1IBatFeW2iZeyltfj-QSw1w?pwd=6pus 提取码:6pus

login/authenticate

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

def login(request):
    # 如果用户未登录,那么request.user是个匿名用户
    # print(111111, request.user)  # AnonymousUser
    if request.method == "POST":
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")
        obj = auth.authenticate(request, username=user, password=pwd)
        # 如果校验成功,返回user对象,否则返回None
        # print(111, obj)
        if obj:
            next = request.GET.get('next')
            # 登录成功之后,通过django auth内置的login将用户信息绑定到request对象上,另外也添加了session
            auth_login(request, obj)
            # 绑定成功后,关于这个用户的信息,可以通过request.user来获取
            # 注意,登录失败的话,request.user是个匿名用户
            # print(request.user)
            # # 如,获取用户的各个字段
            # print(request.user.username)
            # print(request.user.email)
            # print(request.user.is_superuser)
            # print(request.user.is_active)
            # print(request.user.is_staff)
            # 添加了session信息
            # print(request.session)
            # print(request.session.__dict__)
            if next:
                return redirect(next)
            else:
                return redirect('/index/')
        else:
            return render(request, 'login.html', {"msg": "user or password error!"})
    return render(request, 'login.html')

结合上面的views.py中的部分代码。

首先了解一个request.user对象, 这个对象在登录成功之前,它指向的是一个匿名用户AnonymousUser

authenticate

在登录校验中,我们使用的是auth app提供的authenticate(request, username, password)函数进行校验,如果校验成功,返回当前登录的成功的User模型类的对象,否则返回None。

login

登录成功之后,使用的是auth app提供的login(request, obj)函数保存用户信息:

  • 将User模型类的的对象obj绑定到request.user上,我们可以通过request.user点出该模型类对象的各个字段,如用户名、email等。在用户登录成功之后,我们可以在别的视图函数中直接提取用户的相关信息,非常方便。
  • 保存session信息。

另外,@login_required装饰器的内部校验也依赖login方法。

logout

见名知意,logout(request)是auth app提供的专门用来做退出的函数,它干了两件事,清空用户的session信息,将request.user重置为AnonymousUser

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings


@login_required
def logout(request):
    # django auth内置的logout,用于退出登录状态
    # 相当于flush session中的关于用户的登录信息
    auth_logout(request)
    # 退出之后,清空各种session信息和request.user
    print(request.user)  # AnonymousUser
    print(request.session)
    print(request.session.__dict__)
    return redirect('/login/')

@login_required

auth app还提供了一个@login_required装饰器,用于限制某些视图必须登录才能访问。

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings


@login_required
def index(request):
    users_obj = User.objects.all()
    return render(request, 'index.html', {"users_obj": users_obj})

def login(request):
    # 如果用户未登录,那么request.user是个匿名用户
    # print(111111, request.user)  # AnonymousUser
    if request.method == "POST":
        user = request.POST.get("user")
        pwd = request.POST.get("pwd")
        obj = auth.authenticate(request, username=user, password=pwd)
        # 如果校验成功,返回user对象,否则返回None
        # print(111, obj)
        if obj:
            next = request.GET.get('next')
            # 登录成功之后,通过django auth内置的login将用户信息绑定到request对象上,另外也添加了session
            auth_login(request, obj)
            if next:
                return redirect(next)
            else:
                return redirect('/index/')
        else:
            return render(request, 'login.html', {"msg": "user or password error!"})
    return render(request, 'login.html')

@login_required装饰器用法也很简单,加在对应的视图上就行了。

但是!还有需要注意的点,它内部会判断用户是否登录,如果发现未登录,坑来了,它会默认的跳转到/accounts/login/这个路由进行登录,登录成功返回到被装饰的视图上(next=/index/)。

http://127.0.0.1:8000/accounts/login/?next=/index/

但问题来了,我们自己定义的登录路由如果不是/accounts/login/,就会404大黄页!!!所以,解决办法有两个,你把你的登录路由也改为/accounts/login/;要么就是在settings.py中自定义路由:

# settings.py
# 这里配置成你项目登录页面的路由
# 如果这里不配置,那么你的的登录路由需要写成这样/accounts/login/
LOGIN_URL = '/login/'

# urls.py
urlpatterns = [
    path('login/', views.login),  # setting中配置了,可以自定义路由了
    re_path(r'^accounts/login/', views.login),  # settings中不配置,你写成这样的路由
]

is_authenticated

如果用户登录成功,那么request.user.is_authenticated就等于True,可以用来干嘛么....就差不多跟上面的@login_required装饰器类似,我这里简单列个示例:

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

@login_required
def index(request):
    users_obj = User.objects.all()
    return render(request, 'index.html', {"users_obj": users_obj})


def index2(request):
    """
    这个视图就是为了演示is_authenticated的用法
    """
    print('is_login', request.user.is_authenticated)
    # 注意,根据django版本的不同,你要看下is_authenticated源码,是否需要加括号
    # 源码在 from django.contrib.auth.base_user import AbstractBaseUser 类中
    if request.user.is_authenticated:  # 如果登录成功,就正常的执行视图函数的内容
        users_obj = User.objects.all()
        return render(request, 'index.html', {"users_obj": users_obj})
    else:  # 否则就跳转到登录页面进行登录认证
        return redirect('{}?next={}'.format(settings.LOGIN_URL, request.path))

create/create_user/create_superuser

添加用户信息,可以根据需求来使用:

  • create:使用create方法就跟平常我们使用orm插入记录是一样的,但因为auth app的User表的密码字段是密文,所以创建用户时需要注意,User.objects.create(),这样创建的用户密码是明文的,这样会导致你无法通过登录校验,所以需要单独处理。
  • create_user:直接通过该方法创建一个密文的用户。
  • create_superuser:创建一个具有超级管理员权限的用户,它需要三个必传参数,用户名、密码、邮箱。等价于python manage.py createsuperuser创建的超级管理员用户。
from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

def register(request):
    form_obj = AuthModelForm()
    if request.method == "POST":
        form_obj = AuthModelForm(request.POST)
        if form_obj.is_valid():
            # print(form_obj.cleaned_data)
            # {
            #     'username': 'a', 'password': 'aa', 're_password': 'aa',
            #     'email': 'aaa@qq.com',
            #     'is_active': True, 'is_staff': False, 'is_superuser': False
            # }
            # 把无关的字段删除掉
            form_obj.cleaned_data.pop('re_password')
            # ------ 使用普通的create方法创建用户 ------
            # user_obj = User.objects.create(**form_obj.cleaned_data)
            # user_obj.set_password(form_obj.cleaned_data.get('password'))
            # user_obj.save()

            # ------ 使用create_user方法创建用户 ------
            # 由于前端在注册时,是否具有某些权限已经勾选了,所以,这里直接使用create_user就行了,如果is_superuser的值是True,等同于create_superuser
            User.objects.create_user(**form_obj.cleaned_data)

            # ------ 使用create_user方法创建用户 ------
            # 如果要创建超级用户,必须要传下面三个参数,其它字段的值都保持默认即可,这样创建出来的用户就是超级用户
            #
            # User.objects.create_superuser(
            #     username=form_obj.cleaned_data.get('username'),
            #     password=form_obj.cleaned_data.get('password'),
            #     email=form_obj.cleaned_data.get('email'),
            # )
            return redirect('/index/')
        else:
            return render(request, 'register.html', {"form_obj": form_obj, "title": "注册用户"})
    return render(request, 'register.html', {"form_obj": form_obj, "title": "注册用户"})

set_password/check_password

set_passwordcheck_password这两个auth app提供的方法专门用来处理密码相关的,比如用户修改密码,就要首先检查下原密码是否正确,然后再输入新的密码,并将密码进行更新到User表。

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

@login_required
def reset_password(request, pk):
    form_obj = RestPasswordModelForm()
    if request.method == "POST":
        form_obj = RestPasswordModelForm(request.POST)
        if form_obj.is_valid():
            print("reset_password", form_obj.cleaned_data, pk)
            obj = User.objects.filter(pk=pk).first()
            # django auth内置的set_password,用于为指定用户设置一个密文密码
            obj.set_password(raw_password=form_obj.cleaned_data.get("password"))
            obj.save()  # 别忘了save
            return redirect('/index/')
        else:
            return render(request, 'register.html', {"form_obj": form_obj, "title": "重置密码"})
    return render(request, 'register.html', {"form_obj": form_obj, "title": "重置密码"})


@login_required
def check_password(request):
    if request.is_ajax():
        old_pwd = request.POST.get("old_pwd")
        pk = request.POST.get("pk")
        obj = User.objects.filter(pk=pk).first()
        data = {}
        # django auth内置的check_password,用于检查指定用户输入的密码是否正确,正确返回True,否则返回False
        print('check_password', obj.check_password(old_pwd))
        if obj.check_password(old_pwd):
            data['status'] = True
        else:
            data['status'] = False
        return JsonResponse(data)

修改用户信息

这里额外的介绍下如何更新用户的信息。

from django.shortcuts import render, redirect
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

@login_required
def edit(request, pk):
    # 因为涉及到了可能会编辑所有用户的信息,这里呢就不能用request.user了,而是要重新获取下要编辑的用户对象,单独处理
    obj = User.objects.filter(pk=pk).first()
    form_obj = AuthModelForm(instance=obj)
    if request.method == "POST":
        form_obj = AuthModelForm(instance=obj, data=request.POST)
        if form_obj.is_valid():
            print(form_obj.cleaned_data)
            form_obj.cleaned_data.pop('re_password')
            form_obj.save()
            # 上面直接保存的话,密码还是明文的,这样的话,这个用户密码就是非法了,以后无法通过校验了
            # 所以要使用set_password单独处理下,这样明文密码最后保存到数据库就是密文的了
            obj.set_password(form_obj.cleaned_data.get('password'))
            obj.save()  # 别忘了save
            return redirect('/index/')
        else:
            render(request, 'register.html', {"form_obj": form_obj, "title": "编辑用户信息"})
    return render(request, 'register.html', {"form_obj": form_obj, "title": "编辑用户信息"})

主要是密文密码要特殊处理下,其他的没啥。
注意,如果修改的是当前登录的用户自己的用户信息,修改完成后,就需要重新登录。

批量创建用户

也是额外的写个批量创建用户的示例,方便以后使用。

import random
import faker
from django.shortcuts import render, redirect, HttpResponse
from django.http import JsonResponse
from django.contrib import auth
from django.contrib.auth import logout as auth_logout, login as auth_login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from app01.myForms import AuthModelForm, RestPasswordModelForm
from django.conf import settings

def bulk(request):
    """ 批量创建用户 """
    fk = faker.Faker(locale='zh_CN')

    def bulk_create(num):
        user_obj = User(
            username=fk.name() + str(num),  # 防止出现可能的重名
            email=fk.email(),
            is_active=random.choice([True, False]),
            is_staff=random.choice([True, False]),
            is_superuser=random.choice([True, False]),
        )
        user_obj.set_password('123')  # 默认密码 123
        user_obj.save()

    total = 10
    for i in range(total):
        bulk_create(i)
    return HttpResponse("已成功创建 {} 条用户信息".format(total))

扩展auth_user表

如果想在auth app默认的auth_user表现有字段的基础上,添加额外的字段,那么我们就需要手动的在我们自己的app/models.py中进行手动定义模型类,相当于重构auth app 默认User表。

注意,是在现有的字段上扩展添加新的字段。

必要的设置

咱们自己app中的models.py:

from django.db import models
from django.contrib.auth.models import AbstractUser  # 别忘了导入


class UserInfo(AbstractUser):  # 必须继承这个AbstractUser类,并且这个模型类的类名不能叫做User,否则报错
    # 搞一个字段意思下得了
    phone = models.CharField(max_length=11, verbose_name='手机号', default='')

    def __str__(self):
        return self.username

settings.py中要进行声明,不然数据库迁移的时候会报错。

# 我的项目app叫做app01
AUTH_USER_MODEL = 'app01.UserInfo'

如果遇到报错,就删除:

1. 删除自己app\migrations中的000开头的文件
2. 删除下面路径下的000开头的文件
	\Lib\site-packages\django\contrib\admin\migrations
	\Lib\site-packages\django\contrib\auth\migrations

然后再重新执行数据库迁移命令。

注意,想要扩展auth_user表,最好是在首次运行makemigrations和migrate之前进行,否则,经过实测,哪怕不报错,你自定义的模型类也不会被创建!!!那么对于已经执行了数据库迁移命令了怎么办?好办,删库之后,再重新进行数据库迁移,当然,注意数据备份
具体参见:https://docs.djangoproject.com/zh-hans/4.0/topics/auth/customizing/#extending-user

现在我们就不用在使用auth_user这个表了, 而是使用UserInfo模型类和对应的app01_userinfo表了。

而auth app提供的认证功能都照常使用。

如果你参考的是我的代码示例的话,就是将app01/myFormapp01/views中所有的原来的User替换为UserInfo即可,然后modelfrom中添加一个phone字段,用于添加用户信息,其他代码不变。

重写AbstractUser类

当你想要更自由定义字段,又想要使用auth app的功能,那么就在我们自己的app中的models.py中重写AbstractUser类吧。

必要的设置

咱们自己app中的models.py:

from django.db import models
from django.contrib import auth
from django.core.exceptions import PermissionDenied
from django.contrib.auth.models import AbstractUser, AbstractBaseUser, PermissionsMixin, UserManager  # 别忘了导入

# A few helper functions for common logic between User and AnonymousUser.
def _user_get_permissions(user, obj, from_name):
    permissions = set()
    name = 'get_%s_permissions' % from_name
    for backend in auth.get_backends():
        if hasattr(backend, name):
            permissions.update(getattr(backend, name)(user, obj))
    return permissions

def _user_has_perm(user, perm, obj):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if not hasattr(backend, 'has_perm'):
            continue
        try:
            if backend.has_perm(user, perm, obj):
                return True
        except PermissionDenied:
            return False
    return False

def _user_has_module_perms(user, app_label):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if not hasattr(backend, 'has_module_perms'):
            continue
        try:
            if backend.has_module_perms(user, app_label):
                return True
        except PermissionDenied:
            return False
    return False





class UserProfile(AbstractBaseUser, PermissionsMixin):
    # 重写的部分可以参考AbstractUser,看看人家是咋写的
    username = models.CharField(
        max_length=150,
        unique=True,
        help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.',
    )
    phone = models.CharField(max_length=11, verbose_name='手机号', default='')
    # 密码字段AbstractBaseUser中有定义,这里可以省略
    # password = models.CharField(max_length=128, verbose_name='密码')

    email = models.EmailField()

    is_staff = models.BooleanField(default=True)
    is_active = models.BooleanField(default=True)
    is_superuser = models.BooleanField(default=True)



    # 照抄AbstractUser
    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = '用户表'
        verbose_name_plural = "用户表"

    def get_full_name(self):
        # The user is identified by their email address
        return self.username

    def get_short_name(self):
        # The user is identified by their email address
        return self.username

    def __str__(self):  # __unicode__ on Python 2
        return self.username

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

        if self.is_active and self.is_superuser:
            return True
        return _user_has_perm(self, perm, obj)

    def has_perms(self, perm_list, obj=None):
        #     "Does the user have a specific permission?"
        # Simplest possible answer: Yes, always
        for perm in perm_list:
            if not self.has_perm(perm, obj):
                return False
        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
        if self.is_active and self.is_superuser:
            return True

        return _user_has_module_perms(self, app_label)

    # UserProfile.objects.create...这些操作的 objects 就是下面的 objects
    objects = UserManager()

其主要还是翻着人家的源码进行魔改,对于新手来说,难度稍微大些。

settings.py中要进行声明,不然数据库迁移的时候会报错。

# 我的项目app叫做app01
AUTH_USER_MODEL = 'app01.UserProfile'

如果遇到报错,就删除:

1. 删除自己app\migrations中的000开头的文件
2. 删除下面路径下的000开头的文件
	\Lib\site-packages\django\contrib\admin\migrations
	\Lib\site-packages\django\contrib\auth\migrations

然后再重新执行数据库迁移命令。

现在我们就不用在使用auth_user这个表了, 而是使用UserProfile模型类和对应的app01_userprofile表了。

而auth app提供的认证功能都照常使用。

如果你参考的是我的代码示例的话,就是将app01/myFormapp01/views中所有的原来的User替换为UserProfile即可,其他代码基本不变。

that's all, see also:

Django自带的用户认证 | 14 Django的用户认证组件

posted @ 2022-03-15 16:54  听雨危楼  阅读(494)  评论(0编辑  收藏  举报