• <学习笔记>从零开始自学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=wangchongyang@126.com

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

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

    1. Email address: wangchongyang@126.com
    2. Password:
    3. Password (again):
    4. This password is too short. It must contain at least 8 characters.
    5. This password is too common.
    6. Bypass password validation and create user anyway? [y/N]: n
    7. Password:
    8. Password (again):
    9. This password is too common.
    10. Bypass password validation and create user anyway? [y/N]: y
    11. Superuser created successfully.

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

    1.2 创建用户

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

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

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

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

    1.3 在 Web 请求中验证身份

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

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

    1. if request.user.is_authenticated():
    2. # 处理通过身份验证的用户
    3. else:
    4. # 处理匿名用户

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

    2.1 登录与退出

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

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

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

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

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

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

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

    例如:

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

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

    2.2 限制已登录用户的访问

    2.2.1 直接方式

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

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

    2.2.2 装饰器

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

    1. from django.contrib.auth.decorators import login_required
    2. @login_required
    3. def my_view(request):
    4. ...

    login_required() 的作用如下:

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

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

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

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

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

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

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

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

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

    1. from django.contrib.auth import views as auth_views
    2. url(r'^accounts/login/$', auth_views.login),

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

    2.3 permission_required 装饰器

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

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

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

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

    与 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 }} 中:
     

    1. {% if user.is_authenticated %}
    2. <p>Welcome, {{ user.username }}. Thanks for logging in.p>
    3. {% else %}
    4. <p>Welcome, new user. Please log in.p>
    5. {% 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 %} 语句检查权限:

    1. {% if perms.foo %}
    2. <p>You have permission to do something in the foo app.p>
    3. {% if perms.foo.can_vote %}
    4. <p>You can vote!p>
    5. {% endif %} {% if perms.foo.can_drive %}
    6. <p>You can drive!p>
    7. {% endif %} {% else %}
    8. <p>You don't have permission to do anything in the foo app.p>
    9. {% endif %}

    此外,还可以使用 {% if in %} 语句检查权限。

    1. {% if 'foo' in perms %}
    2. {% if 'foo.can_vote' in perms %}
    3. <p>In lookup works, too.p>
    4. {% endif %}
    5. {% endif %}

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

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

    3.1 登录系统

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

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>登录title>
    6. head>
    7. <body>
    8. <div class="container" style="margin-top: 100px">
    9. <div class="row">
    10. <div>
    11. <div>
    12. <div>
    13. <h3>登录h3>
    14. div>
    15. <div>
    16. <form action="" method="post" novalidate>
    17. {% csrf_token %}
    18. {# 方法一: 写出form表单中的指定字段 #}
    19. {# {{ login_form.username.label_tag }}:#}
    20. {# {{ login_form.username }}#}
    21. {# <p>{{ login_form.errors.username.0 }}p>#}
    22. {# {{ login_form.password.label_tag }}:#}
    23. {# {{ login_form.password }}#}
    24. {# <p>{{ login_form.errors.password.0 }}p>#}
    25. {# <div>{{ login_form.non_field_errors }}div>#}
    26. {# 方法二: 遍历出form表单中的所有字段 #}
    27. {% for field in login_form %}
    28. {{ field.label_tag }}
    29. {{ field }}
    30. <p>{{ field.errors.as_text }}p>
    31. {% endfor %}
    32. {# 此处错误信息会返回clean联合校验,也就是非单个字段校验的错误信息#}
    33. <span>{{ login_form.non_field_errors }}span>
    34. <input type="submit" value="登录" class="btn">
    35. form>
    36. div>
    37. div>
    38. div>
    39. div>
    40. div>
    41. <br>
    42. <div>
    43. <a href="/register">注册a>
    44. div>
    45. body>
    46. html>

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

    1. from django import forms
    2. from django.contrib.auth import authenticate
    3. from django.contrib.auth.models import User
    4. class LoginForm(forms.Form):
    5. username = forms.CharField(
    6. label="用户名",
    7. min_length=3,
    8. widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入用户名"}),
    9. error_messages={
    10. "required": "用户名不能为空",
    11. "min_length": "用户名最小长度为3位",
    12. },
    13. )
    14. password = forms.CharField(
    15. label="密码",
    16. min_length=6,
    17. error_messages={
    18. "min_length": "密码最小长度为6位",
    19. "required": "密码不能为空",
    20. },
    21. widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "请输入密码"}),
    22. )
    23. def clean(self):
    24. username = self.cleaned_data.get("username", "")
    25. password = self.cleaned_data.get("password", "")
    26. user = authenticate(username=username, password=password)
    27. if not user:
    28. raise forms.ValidationError("用户名或密码错误")
    29. self.cleaned_data["user"] = user
    30. return self.cleaned_data

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

    1. def user_login(request):
    2. if request.method == "GET":
    3. username = request.GET.get("username")
    4. login_form = LoginForm()
    5. return render(request, "login.html", context={"login_form": login_form})
    6. elif request.method == "POST":
    7. login_form = LoginForm(request.POST)
    8. if login_form.is_valid():
    9. # 注意:验证用户名和密码是否正确放到forms中去验证了
    10. # login(request, request.user) # 此处不能使用request.user,因为他还没有验证,是匿名用户
    11. # 所以需要在form中校验通过后传递过来user
    12. login(request, login_form.cleaned_data["user"])
    13. user = request.POST["username"]
    14. next = request.GET.get("next", reverse("index"))
    15. return redirect(next,{'user':user})
    16. else:
    17. 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

    1. html>
    2. <html lang="en">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>注册title>
    6. head>
    7. <body>
    8. <div class="container" style="margin-top: 100px">
    9. <div class="row">
    10. <div>
    11. <div>
    12. <div>
    13. <h3>注册h3>
    14. div>
    15. <div>
    16. <form action="" method="post" novalidate>
    17. {% csrf_token %}
    18. {% for field in register_form %}
    19. {{ field.label_tag }}
    20. {{ field }}
    21. <p>{{ field.errors.as_text }}p>
    22. {% endfor %}
    23. <span>{{ register_form.non_field_errors }}span>
    24. <input type="submit" value="注册">
    25. form>
    26. div>
    27. div>
    28. div>
    29. div>
    30. div>
    31. body>
    32. html>

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

    1. class RegisterForm(LoginForm):
    2. password_again = forms.CharField(
    3. label="确认密码",
    4. min_length=6,
    5. error_messages={
    6. "required": "确认密码不能为空",
    7. "min_length": "密码最小长度为6位",
    8. },
    9. widget=forms.PasswordInput(attrs={"class": "form-control", "placeholder": "确认密码"}),
    10. )
    11. email = forms.EmailField(
    12. label="邮箱",
    13. required=False,
    14. error_messages={
    15. "required": "密码不能为空",
    16. },
    17. widget=forms.EmailInput(attrs={"class": "form-control", "placeholder": "请输入邮箱"})
    18. )
    19. def clean_username(self):
    20. username = self.cleaned_data["username"]
    21. if User.objects.filter(username=username).exists():
    22. raise forms.ValidationError("用户名已存在")
    23. return username
    24. def clean_email(self):
    25. email = self.cleaned_data["email"]
    26. if email and User.objects.filter(email=email).exists():
    27. raise forms.ValidationError("邮箱已存在")
    28. return email
    29. def clean(self):
    30. password = self.cleaned_data.get("password", "")
    31. password_again = self.cleaned_data.get("password_again", "")
    32. if password != password_again:
    33. raise forms.ValidationError("两次密码不一致,请重新输入")
    34. return self.cleaned_data

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

    1. def user_register(request):
    2. if request.method == "GET":
    3. register_form = RegisterForm()
    4. return render(request, "register.html", context={"register_form": register_form})
    5. elif request.method == "POST":
    6. register_form = RegisterForm(request.POST)
    7. if register_form.is_valid():
    8. username = register_form.cleaned_data["username"]
    9. password = register_form.cleaned_data["password"]
    10. email = register_form.cleaned_data.get("email", "")
    11. # 创建用户
    12. user = User.objects.create_user(username, email, password)
    13. next = request.GET.get("next", reverse("index"))
    14. return redirect(next)
    15. else:
    16. 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功能非常容易

    1. def user_logout(request):
    2.     logout(request)
    3.     return redirect("login")

    3.4 设置权限

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

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

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

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

    1. from django.contrib.auth.decorators import permission_required
    2. @permission_required('classManage.add_student',login_url='/login/')
    3. def add_student(request):
    4. if request.method == 'GET':
    5. student_list = addStudent()
    6. return render(request,'addStudent.html',{'students':student_list},status=200)
    7. else:
    8. student_list = addStudent(request.POST)
    9. if student_list.is_valid():
    10. student_list.save()
    11. return render(request,'addStudent.html',{'student_list':student_list},status=200)

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

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

    1. from classManage.models import Student
    2. from django.contrib.auth.models import Group, Permission
    3. from django.contrib.contenttypes.models import ContentType
    4. user = User.objects.get(username='testone')
    5. permission = Permission.objects.get(codename='add_student')
    6. user.user_permissions.add(permission)

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

    发现可以访问了

     

  • 相关阅读:
    ssm+爱尚购物 毕业设计-附源码211622
    第十课 贪心
    淘宝官方开放平台API接口获得店铺的所有商品、商品id、商品标题、销量参数调用示例
    经典论文《Efficient Estimation of Word Representations in Vector Space》学习笔记
    RenduCore笔记-c++实用库
    安全扫描项目
    MMQA5V6T1G 瞬态电压抑制器 TVS二极管的特性及应用
    【源码】第10期|configstore 持久化配置存储
    Iphone自动化指令每隔固定天数打开闹钟关闭闹钟(二)
    二级导航栏
  • 原文地址:https://blog.csdn.net/qq_41597915/article/details/127754523