<学习笔记>从零开始自学Python-之-web应用框架Django( 十一)用户系统和身份验证

用户系统是现代网站的重要组成部分,对用户进行分组权限管理是非常必要的。

Django内置了一套用户和身份验证系统,不用太多代码开发就可以使用这个系统。

Django 的身份验证系统包括:

• 用户

• 权限:二元(是或否)旗标,指明用户是否能执行特定的任务

• 分组:把标注和权限赋予多个用户的通用方式

• 可配置的密码哈希系统

• 管理身份验证和权限核准的表单

• 登录用户或限制内容的视图工具

• 可更换的后端系统

Django 的身份验证系统十分通用,没有提供 Web 身份验证系统中某些常用的功能。

某些常用功能通过第三 方包实现:

• 密码强度检查

• 登录尝试次数限制

• 通过第三方验证身份(如 OAuth)

1、User对象

User 对象是这个身份验证系统的核心,通常用于标识与网站交互的人,还用于限制访问、记录用户资料,以 及把内容与创建人关联起来,等等。

在 Django 的身份验证框架中,只有一个用户类存在,因此 superusers 或管理后台的 staff 用户只是设定了特殊属性的用户对象,而不是分属不同类的用户对象。

默认用户主要有 下面几个属性:

• username

• password

• email

• first_name

• last_name

1.1 创建超级用户

超级用户使用 createsuperuser 命令创建:

python manage.py createsuperuser --username=teacherWang [email protected]

上述命令会提示你输入密码。输入密码后,立即创建指定的超级用户。

如果没有指定 --username 或 --email 选项,会提示你输入这两个值。

Email address: [email protected]
Password: 
Password (again):
This password is too short. It must contain at least 8 characters.
This password is too common.
Bypass password validation and create user anyway? [y/N]: n
Password: 
Password (again):
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.

最终看到 Superuser created successfully说明创建成功了。

1.2 创建用户

创建和管理用户最简单、最不易出错的方式是使用 Django 管理后台。

管理后台的使用我们在第七章((1条消息) <学习笔记>从零开始自学Python-之-web应用框架Django( 七)Django管理后台_阿尔法羊的博客-CSDN博客)详细讲过了

当然也可以用Django内置的辅助函数create_user()来实现同样的功能

from django.contrib.auth.models import User 
user = User.objects.create_user('teacherWang', '[email protected]', 'password') 
# 此时,user 是一个 User 对象,而且已经保存到数据库中 
# 如果想修改其他字段的值,可以继续修改属性 
user.last_name = 'wang' 
user.save()

1.3 在 Web 请求中验证身份

Django 使用会话和中间件把身份验证系统插入 request 对象,为每个请求提供 request.user 属性,表示当前用户。如果未登陆,这个属性的值是一个 AnonymousUser 实例,否则是是一个 User 实例。

这两种情况可以使 用 is_authenticated() 方法区分,例如:

if request.user.is_authenticated(): 
    # 处理通过身份验证的用户 

else: 
    # 处理匿名用户

2、在视图中操作身份验证

2.1 登录与退出

2.1.1 在视图中使用 login() 登录用户。

它的参数是一个 HttpRequest 对象和一个 User 对象。login() 使用 Django 的会话框架把用户的 ID 保存到会话中。注意,匿名期间设定的会话数据在用户登录后依然存在。

下述示例 展示 authenticate() 和 login() 的用法:

from django.contrib.auth import authenticate, login 
def my_view(request): 
    username = request.POST['username'] 
    password = request.POST['password']
    user = authenticate(username=username, password=password) 
    if user is not None: 
        if user.is_active: 
            login(request, user) 
            # 重定向到成功登录页面 
        else: 
            # 返回“账户未激活”错误消息 
    else: 
        # 返回“无效登录”错误消息

注意:自己动手登录用户时,必须在 login() 之前调用 authenticate()。authenticate() 在 User 对象 上设定一个属性,指明成功验证用户身份的是哪个身份验证后端,而登录过程中需要使用这个信息。如果直接登录从数据库中检索的用户对象,Django 会报错。

2.1.2 在视图中使用logout()退出

在视图中退出通过 login() 登录的用户使用 logout()。这个函数的参数是一个 HttpRequest 对象,而且没有返回值。

例如:

from django.contrib.auth import logout 
def logout_view(request): 
    logout(request) 
    # 重定向到成功退出页面 

注意,如果用户未登录,logout() 函数不报错。调用 logout() 函数后,当前请求的会话数据完全清除,所有 数据将被删除。这样能避免其他人在登录的 Web 浏览器中访问用户之前的会话数据。 如果想让会话中的数据在退出后依然可用,调用 logout() 函数之后再把数据存入会话。

2.2 限制已登录用户的访问

2.2.1 直接方式

限制访问页面简单直接的方式是检查 request.user.is_authenticated(),如果未通过,可以重定向到登录页面:

from django.shortcuts import redirect 
def my_view(request): 
    if not request.user.is_authenticated(): 
        return redirect('/login/?next=%s' % request.path) 
        
# ... 也可以显示一个错误消息: 
from django.shortcuts import render 
def my_view(request): 
    if not request.user.is_authenticated():
        return render(request, 'books/login_error.html') 

2.2.2 装饰器

也可以使用便利的 login_required() 装饰器:

from django.contrib.auth.decorators import login_required 

@login_required 
def my_view(request): 
    ...

login_required() 的作用如下:

• 如果用户未登录,重定向到 LOGIN_URL,并把当前绝对路径添加到查询字符串中。

例如:/accounts/ login/?next=/reviews/3/。

• 如果用户已登录,正常执行视图。视图代码可以放心假定用户已登录。

默认,成功通过身份验证后重定向的目标路径存储在名为 next 的查询字符串参数中。如果想为这个参数提供 其他名称,可以设定 login_required() 可选的 redirect_field_name 参数:

from django.contrib.auth.decorators import login_required
 
@login_required(redirect_field_name='my_redirect_field') 
def my_view(request): 
    ...

注意,如果为 redirect_field_name 提供了值,可能还要定制登录模板,因为模板上下文中存储重定向路径的变量名是 redirect_field_name 的值,而不再是默认的 next。

login_required() 还有个可选的 login_url 参 数。例如:

from django.contrib.auth.decorators import login_required
 
@login_required(login_url='/accounts/login/') 
def my_view(request): 
    ...

注意,如果不指定 login_url 参数,要确保把 LOGIN_URL 设为正确的登录视图。例如,使用默认配置时,要 把下述代码添加到 URL 配置中:

from django.contrib.auth import views as auth_views 

url(r'^accounts/login/$', auth_views.login),

LOGIN_URL 的值还可以是视图函数名称或具名 URL 模式。这样,无需修改设置就可以在 URL 配置中自由映射登录视图。

2.3 permission_required 装饰器

检查用户有没有特定权限是比较常见的任务。鉴于此,Django 提供了一种简便的方式——permission_required() 装饰器:

from django.contrib.auth.decorators import permission_required @permission_required('reviews.can_vote') 
def my_view(request): 
    ...

与 has_perm() 方法一样,参数名称的形式为“.”(例如,reviews.can_vote 是 reviews 应用中某个模型定义的权限)。这个装饰器的参数也可以是一个权限列表。注意,permission_required() 也有可选的 login_url 参数。例如:

from django.contrib.auth.decorators import permission_required @permission_required('reviews.can_vote', login_url='/loginpage/') 
def my_view(request):
    ...

与 login_required() 装饰器一样,login_url 的默认值是 LOGIN_URL。如果指定了 raise_exception 参数,这 个装饰器不会重定向到登录页面,而是抛出 PermissionDenied 异常,显示 403(HTTP Forbidden)视图。

2.4、身份验证视图

Django 为登录、退出和密码管理提供了视图。这些视图使用 auth 包中内置的表单,不过也可以传入自己编写的视图。Django 没有为身份验证视图提供默认的模板,不过下文将说明各个视图的模板上下文。

在项目中使用这些视图要实现不同的方法,不过最简单也是最常见的做法是把 django.contrib.auth.urls 提 供的 URL 配置添加到项目的 URL 配置中。例如:

urlpatterns = [url('^', include('django.contrib.auth.urls'))]

这样,各个视图在默认的 URL 上(后文详述)。 这些内置的视图都返回一个 TemplateResponse 实例,这样便于在渲染之前定制响应数据。多数内置的身份验 证视图提供了 URL 名称,易于引用。

2.4.1 login 视图

登录用户。

默认 URL:/login/。

可选参数:

• template_name:这个视图使用的模板名称。默认为 registration/login.html。

• redirect_field_name:GET 参数中指定登录后重定向 URL 的字段名称。默认为 next。

• authentication_form:验证身份的可调用对象(通常是一个表单类)。默认为 AuthenticationForm。

• current_app:一个提示,指明当前视图所在的应用。

• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。 login 视图的作用如下:

• 如果通过 GET 调用,显示登录表单,其目标地址与当前 URL 一样。稍后详述。

• 如果通过 POST 调用,发送用户提交的凭据,尝试登录用户。如果登录成功,重定向到 next 指定的 URL。如果没有 next,重定向到 LOGIN_REDIRECT_URL(默认为 /accounts/profile/)。如果登录失败,重新显示登录表单。 登录视图的模板由你提供,模板文件默认名为 registration/login.html。

模板上下文:

• form:表示 AuthenticationForm 的 Form 对象。

• next:成功登录后重定向的目标 URL。自身可能也包含查询字符串。

• site:当前 Site,根据 SITE_ID 设置确定。如果未安装网站框架,其值默认为 RequestSite 实例,从 当前 HttpRequest 对象中获取网站名称和域名。

• site_name:site.name 的别名。如果未安装网站框架,其值为 request.META['SERVER_NAME'] 的值。 如果不想把模板命名为 registration/login.html,可以为 URL 配置提供额外的参数,设定 template_name 参数。

2.4.2 logout 视图

退出用户。

默认 URL:/logout/

可选的参数:

• next_page:退出后重定向的目标 URL。

• template_name:一个模板全名,在用户退出后显示。如果未提供这个参数,默认为 registration/ logged_out.html。

• redirect_field_name:GET 参数中指定退出后重定向 URL 的字段名称。默认为 next。如果提供这个参数,next_page 将被覆盖。

• current_app:一个提示,指明当前视图所在的应用。

• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。

模板上下文:

• title:本地化之后的字符串“Logged out”。

• site:当前 Site,根据 SITE_ID 设置确定。如果未安装网站框架,其值默认为 RequestSite 实例,从 当前 HttpRequest 对象中获取网站名称和域名。

• site_name:site.name 的别名。如果未安装网站框架,其值为 request.META['SERVER_NAME'] 的值。

2.4.3 logout_then_login 视图

退出用户,然后重定向到登录页面。

默认 URL:未提供。

可选的参数:

• login_url:重定向到的登录页面的 URL。如果未提供,默认为 LOGIN_URL。

• current_app:一个提示,指明当前视图所在的应用。

• extra_context:一个字典,包含额外的上下文数据,随默认的上下文数据一起传给模板。

2.5 内置的表单

如果不想使用内置的视图,也不想为这些功能编写表单,可以使用身份验证系统在 django.contrib.auth.forms 中内置的几个表单(表 11-1)。 这些内置的表单对用户模型有些假设,如果自定义了用户模型,可能要自己动手为身份验证系统编写表单。

Django 内置的身份验证表单
表单名 说明
AdminPasswordChangeForm 在管理后台中用于修改用户密码的表单。第一个位置参数是用户对象。
AuthenticationForm 登录表单。请求对象是第一个位置参数,存储在表单中,供子类使用。
PasswordChangeForm 修改密码的表单。
PasswordResetForm 用于生成并发送带有重设密码链接的电子邮件。
SetPasswordForm 让用户修改密码的表单,无需输入旧密码。
UserChangeForm 在管理后台中用于修改用户信息和权限的表单。
UserCreationForm 创建新用户的表单。

2.6 模板中的身份验证数据

使用 RequestContext 时,当前登录用户及其权限可通过模板上下文访问。

2.6.1 用户

渲染模板的 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 %}

如果使用的不是 RequestContext,这个模板上下文变量不可用。

2.6.2 权限

当前登录用户的权限存储在模板变量 { { perms }} 中。它的值是 django.contrib.auth.context_processors.PermWrapper 的一个实例,对模板友好。在 { { perms }} 对象中,单属性查找由 User.has_module_perms 代理。只要当前登录用户在 foo 应用中有权限,下述示例就返回 True:

{ { perms.foo }}

两层属性查找由 User.has_perm 代理。如果当前登录用户有 foo.can_vote 权限,下述示例返回 True:

{ { perms.foo.can_vote }}

因此,在模板中可以使用 {% if %} 语句检查权限:

{% if perms.foo %}
<p>You have permission to do something in the foo app.</p>
{% if perms.foo.can_vote %}
<p>You can vote!</p>
{% endif %} {% if perms.foo.can_drive %}
<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.can_vote' in perms %}
<p>In lookup works, too.</p>
{% endif %}
{% endif %}

3、案例:自己动手做一个网站用户系统

基于我们前面已经做好的网站,我们现在做一个简单的用户登录和注册系统,然后设置权限

3.1 登录系统

先写一个简单的登录页面login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<div class="container" style="margin-top: 100px">
    <div class="row">
        <div>
            <div>
                <div>
                    <h3>登录</h3>
                </div>
                <div>
                    <form action="" method="post" novalidate>
                        {% csrf_token %}
 
{#                        方法一: 写出form表单中的指定字段 #}
{#                        {
   
   { login_form.username.label_tag }}:#}
{#                        {
   
   { login_form.username }}#}
{#                        <p>{
   
   { login_form.errors.username.0 }}</p>#}
{#                        {
   
   { login_form.password.label_tag }}:#}
{#                        {
   
   { login_form.password }}#}
{#                        <p>{
   
   { login_form.errors.password.0 }}</p>#}
{#                        <div>{
   
   { login_form.non_field_errors }}</div>#}
 
{#                        方法二: 遍历出form表单中的所有字段 #}
                        {% for field in login_form %}
                            {
   
   { field.label_tag }}
                            {
   
   { field }}
                            <p>{
   
   { field.errors.as_text }}</p>
                        {% endfor %}
{#                        此处错误信息会返回clean联合校验,也就是非单个字段校验的错误信息#}
                        <span>{
   
   { login_form.non_field_errors }}</span>
 
                        <input type="submit" value="登录" class="btn">
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
<br>
<div>
    <a href="/register">注册</a>
</div>
</body>
</html>

然后我们在forms.py里面创建一个LoginForm类,用于接收前端表单输入的数据,并实现验证

from django import forms
from django.contrib.auth import authenticate
from django.contrib.auth.models import User
 
 
class LoginForm(forms.Form):
    username = forms.CharField(
        label="用户名",
        min_length=3,
        widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入用户名"}),
        error_messages={
            "required": "用户名不能为空",
            "min_length": "用户名最小长度为3位",
        },
    )
    password = forms.CharField(
        label="密码",
        min_length=6,
        error_messages={
            "min_length": "密码最小长度为6位",
            "required": "密码不能为空",
        },
        widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "请输入密码"}),
    )
 
    def clean(self):
        username = self.cleaned_data.get("username", "")
        password = self.cleaned_data.get("password", "")
        user = authenticate(username=username, password=password)
        if not user:
            raise forms.ValidationError("用户名或密码错误")
 
        self.cleaned_data["user"] = user
        return self.cleaned_data

然后在views.py 里面写好一个登录的函数

def user_login(request):
    if request.method == "GET":
        username = request.GET.get("username")
        login_form = LoginForm()
        return render(request, "login.html", context={"login_form": login_form})
    elif request.method == "POST":
        login_form = LoginForm(request.POST)
        if login_form.is_valid():
            # 注意:验证用户名和密码是否正确放到forms中去验证了
            # login(request, request.user)  # 此处不能使用request.user,因为他还没有验证,是匿名用户
            # 所以需要在form中校验通过后传递过来user
            login(request, login_form.cleaned_data["user"])
            user = request.POST["username"]
            next = request.GET.get("next", reverse("index"))
            return redirect(next,{'user':user})
        else:
            return render(request, "login.html", {"login_form": login_form})

如果从form类返回的验证正确,就跳转到指定网页,这里指定到 index 页面,注意这里这个 index 是 路由的名字(name,就是设置path时候指定的name)。

最后在urls.py 里面加上路由

path('login/',user_login,name="login"),

我们访问http://127.0.0.1:8000/login

 因为没有加上css,界面看上去比较朴素,但是已经可以使用了

3.2 注册系统

当然这时候我们只有一个前面创建的超级用户,要想让普通用户注册使用,还要做一个注册系统

一样,先写好前端的表单页面 register.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册</title>
</head>
<body>
<div class="container" style="margin-top: 100px">
    <div class="row">
        <div>
            <div>
                <div>
                    <h3>注册</h3>
                </div>
                <div>
                    <form action="" method="post" novalidate>
 
                        {% csrf_token %}
                        {% for field in register_form %}
                            {
   
   { field.label_tag }}
                            {
   
   { field }}
                            <p>{
   
   { field.errors.as_text }}</p>
                        {% endfor %}
                        <span>{
   
   { register_form.non_field_errors }}</span>
                        <input type="submit" value="注册">
 
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

在forms.py里面写一个注册用的 RegisterForm类

class RegisterForm(LoginForm):
    password_again = forms.CharField(
        label="确认密码",
        min_length=6,
        error_messages={
            "required": "确认密码不能为空",
            "min_length": "密码最小长度为6位",
        },
        widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "确认密码"}),
    )
    email = forms.EmailField(
        label="邮箱",
        required=False,
        error_messages={
            "required": "密码不能为空",
        },
        widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": "请输入邮箱"})
    )
 
    def clean_username(self):
        username = self.cleaned_data["username"]
        if User.objects.filter(username=username).exists():
            raise forms.ValidationError("用户名已存在")
        return username
 
    def clean_email(self):
        email = self.cleaned_data["email"]
        if email and User.objects.filter(email=email).exists():
            raise forms.ValidationError("邮箱已存在")
 
        return email
 
    def clean(self):
        password = self.cleaned_data.get("password", "")
        password_again = self.cleaned_data.get("password_again", "")
 
        if password != password_again:
            raise forms.ValidationError("两次密码不一致,请重新输入")
 
        return self.cleaned_data

再在views.py里面 写好视图函数

def user_register(request):
    if request.method == "GET":
        register_form = RegisterForm()
        return render(request, "register.html", context={"register_form": register_form})
    elif request.method == "POST":
        register_form = RegisterForm(request.POST)
        if register_form.is_valid():
            username = register_form.cleaned_data["username"]
            password = register_form.cleaned_data["password"]
            email = register_form.cleaned_data.get("email", "")
            # 创建用户
            user = User.objects.create_user(username, email, password)
            next = request.GET.get("next", reverse("index"))
            return redirect(next)
        else:
            return render(request, "register.html", {"register_form": register_form})

最后加上路由

path('register/',user_register,name="register"),

我们访问 http://127.0.0.1:8000/register

我们注册一个名为testone的用户,登录之后可以看到, 用户名正确显示出来了

 

3.3 退出登录

 这里我们前端加了一个退出登录的功能按钮

Django内置了一个logout 函数,实现logout功能非常容易

def user_logout(request):

    logout(request)

    return redirect("login")

3.4 设置权限

既然使用了用户系统,当然对用户访问就要做些限制,我们在主页视图函数前加上装饰器,并指定如果未登录时候的跳转页面(如果不指定,默认的跳转页面是('/accounts/login/')

from django.contrib.auth.decorators import login_required
@login_required(login_url='/login/')
def classnotice(request):
    print(request.GET)
    now=datetime.datetime.now()
    context={}
    context['student_list']=Student.objects.all().order_by('-score')
    context['teacher']='王重阳'
    context['now']=now
    
    return render(request,'class3.html',context=context,status=200)

再访问主页,就会跳转到登录页面

最后我们再来设置一下权限。比如之前我们做了一个add_student的表单,用于增加Student模型的实例。现在我们给这个功能加上权限,这样没有权限的用户就会退到登录界面

from django.contrib.auth.decorators import permission_required
@permission_required('classManage.add_student',login_url='/login/')
def add_student(request):
    if request.method == 'GET':
        student_list = addStudent()
        return render(request,'addStudent.html',{'students':student_list},status=200)
    else:
        student_list = addStudent(request.POST)
        if student_list.is_valid():
            student_list.save()
        return render(request,'addStudent.html',{'student_list':student_list},status=200)

这时候,我们用teacherWang账户登录可以访问 ‘/add_student/’,但是用testone账户访问'/add_student'就会跳转到登录界面。

我们用程序给testone账户赋予权限

from classManage.models import Student
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType

user = User.objects.get(username='testone')
permission = Permission.objects.get(codename='add_student')
user.user_permissions.add(permission)

然后再用testone账户登录,再访问http://127.0.0.1:8000/add_student

发现可以访问了

猜你喜欢

转载自blog.csdn.net/qq_41597915/article/details/127754523