Django 开发 MxOnline 项目笔记 -- 第6章 用

2018-03-05  本文已影响1113人  江湖十年

一、用户登录

{% load staticfiles %}
<link rel="stylesheet" type="text/css" href="../css/reset.css">
<link rel="stylesheet" type="text/css" href="{% static "css/reset.css" %}">
# mxonline/urls.py

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", TemplateView.as_view(template_name="index.html"), name="index"),
    path("login/", TemplateView.as_view(template_name="login.html"), name="login"),
]
# apps/users/views.py

from django.shortcuts import render
from django.contrib.auth import authenticate, login


def user_login(request):
    if request.method == "POST":
        # 如果是 POST 请求,先获取用户通过 form 表单提交过来的用户名和密码
        user_name = request.POST.get("username", "")
        user_password = request.POST.get("password", "")
        # 在得到用户名和密码之后,登录之前需要验证用户名和密码是否合法(即已存储在数据库当中)
        # django 为我们提供了一个 authenticate 方法,它可以向数据库发起验证,用来验证 用户名和密码 是否正确,
        # 调用此方法需要传递两个命名参数 username 和 password,
        # 如果 用户名和密码是合法的,此方法返回一个 user 对象
        # 如果 用户名和密码不合法,则返回 None
        user = authenticate(username=user_name, password=user_password)
        if user is not None:
            # 如果用户名和密码合法,进行登录,django 为我们提供了 login 方法可以完成登录
            # 调用 django 的 login 函数,来完成用户的登录
            # 此函数接收两个参数, request 和 user 对象
            login(request, user)
            # 登录成功,一般跳转到首页或者个人中心
            return render(request, "index.html")
        else:
            # 如果用户名和密码不合法,返回登录页面,并提示错误信息
            context = {"err_msg": "用户名或密码不正确!"}
            return render(request, "login.html", context)

    elif request.method == "GET":
        # 如果是 GET 请求,返回登录页面
        context = {}
        return render(request, "login.html", context)
# mxonline/urls.py

from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView

from users.views import user_login


urlpatterns = [
    path('admin/', admin.site.urls),
    path("", TemplateView.as_view(template_name="index.html"), name="index"),
    path("login/", user_login, name="login"),
]
request.user.is_authenticated # 代表是用户已经登录状态
# templates/index.html
...
<!-- 判断用户是否登录 -->
{% if request.user.is_authenticated %}
    <!-- 登录成功 -->
    <div class="personal">
        <dl class="user fr">
            <dd>pythonic<img class="down fr" src="{% static "images/top_down.png" %}"/></dd>
            <dt><img width="20" height="20" src="{% static "media/image/2016/12/default_big_14.png" %}"/></dt>
        </dl>
        <div class="userdetail">
            <dl>
            <dt><img width="80" height="80" src="{% static "media/image/2016/12/default_big_14.png" %}"/></dt>
            <dd>
                <h2>django</h2>
                <p>pythonic</p>
            </dd>
            </dl>
            <div class="btn">
                <a class="personcenter fl" href="usercenter-info.html">进入个人中心</a>
                <a class="fr" href="/logout/">退出</a>
            </div>
        </div>
    </div>
{% else %}
    <!-- 未登录 -->
    <a style="color:white" class="fr registerbtn" href="register.html">注册</a>
    <a style="color:white" class="fr loginbtn" href="{% url "login" %}">登录</a>
{% endif %}
...
003.png
扩展用户登录
# mxonline/settings.py

# 重载项目的 AUTHENTICATION_BACKENDS 变量
AUTHENTICATION_BACKENDS = [
    "users.views.CustomBackend"
]
# apps/users/views.py

...
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q

from .models import UserProfile


class CustomBackend(ModelBackend):
    """
    自定义 CustomBackend 类, 用来实现邮箱登录, 继承自 django 的 ModelBackend,
    ModelBackend 类有一个 authenticate 方法, 会被 django 自动调用
    """
    def authenticate(self, request, username=None, password=None, **kwargs):
        # 这里覆写 authenticate 方法, 在此完成自己的后台逻辑, 实现可以通过用户名或邮箱登录网站
        try:
            # 首先根据用户名和密码来查询一下这个用户是否存在于数据库
            # 因为用户名在数据库中是不能重名的, 所以可以用 .get() 方法来查询
            # django 中, 如果直接写 .get(username=username, email=username),
            # 这个查询结果是"并集", 如果想使用"或", 即"username=username 或 email=username",
            # 则可以利用 django 的 Q 对象, 它允许使用|(OR)和&(AND)操作构建复杂的数据库查询
            user = UserProfile.objects.get(Q(username=username) | Q(email=username))
            # 这里查询密码的方式是不能和上面查询用户名一样的,
            # 因为 django 在将密码存储到数据库中是加密的,
            # 所以不能简单的使用 objects.get(password=password) 来查询,
            # 前台 request 传进来的明文 password 是不可能和数据库中加密的 password 匹配的, 所以无法查询,
            # 不过 因为 UserProfile 继承自 django 的 AbstractUser,
            # 而 AbstractUser 有一个 check_password 方法, 可以将传进去的明文 password 进行加密处理,
            # 然后和 user 对象的 password 字段做对比, 验证密码是否和这个用户的密码
            if user.check_password(password):
                # 如果判断成功, 即用户名和密码相匹配, 返回 user 对象
                return user
        except Exception as err:
            # 遇到异常, 即用户名或密码不匹配, 返回 None
            return None
...
将登录视图函数修改成类视图
# apps/users/views.py

...
from django.views.generic.base import View
...
class LoginView(View):
    """
    用户登录类视图
    将上面的用户登录视图函数修改为类视图的形式, 继承 django 的 View 类,
    在这里重新定义 get、post 方法, django 会根据 request 的方法而自动调用相应方法,
    django 自动判断 request.method 为 GET 时, 会自动调用 get 方法, POST 同理.
    """
    def get(self, request):
        context = {}
        return render(request, "login.html", context)

    def post(self, request):
        user_name = request.POST.get("username", "")
        user_password = request.POST.get("password", "")

        user = authenticate(username=user_name, password=user_password)
        if user is not None:
            login(request, user)
            return render(request, "index.html")

...
# mxonline/urls.py

...
from users.views import LoginView


urlpatterns = [
    ...
    path("login/", LoginView.as_view(), name="login"),
]
利用 django form 表单验证用户登录
# apps/users/forms.py

from django import forms


class LoginForm(forms.Form):
    """
    用户登录表单, 需要继承 django 的 forms.Form 类
    """
    # CharField 告诉 form, username 和 password 是 CharField 类型字段,
    # required=True 告诉 form 此字段为必填字段,
    # min_length=6, max_length=18 分别指定了最小长度和最大长度,
    # 如果不满足验证要求, 是不会去查询数据库的, 减少数据库查询负担, 并且返回错误信息,
    # username 和 password 这两个需要 form 来做验证的字段名称,
    # 必须和前端模板中 form 表单里传递过来的字段名称(即 input 标签的 name 属性值)相同,
    # 不然 form 是不会做验证的
    username = forms.CharField(required=True)
    password = forms.CharField(required=True, min_length=6, max_length=18)
# apps/users/views.py
...
from .forms import LoginForm


class LoginView(View):
    """
    用户登录类视图
    将上面的用户登录视图函数修改为类视图的形式, 继承 django 的 View 类,
    在这里重新定义 get、post 方法, django 会根据 request 的方法而自动调用相应方法,
    django 自动判断 request.method 为 GET 时, 会自动调用 get 方法, POST 同理.
    """
    def get(self, request):
        context = {}
        return render(request, "login.html", context)

    def post(self, request):
        # 首先实例化我们定义的 LoginForm, LoginForm 需要一个字典作为参数
        # 这里可以直接把 request.POST 传进来, request.POST 本身就是一个字典
        # 实例化后, 其实 django 已经通过 form 自动完成了 username 和 password 的验证
        login_form = LoginForm(request.POST)
        if login_form.is_valid():
            # login_form 的 is_valid() 方法, 可以判断前端提交过来的字段是否符合定义,
            # is_valid 方法实际上是判断 errors 是否为空, 如果为空, 说明没有错误,
            # 如果 errors 不为空, 返回错误信息
            user_name = request.POST.get("username", "")
            user_password = request.POST.get("password", "")
            user = authenticate(username=user_name, password=user_password)
            if user is not None:
                login(request, user)
                return render(request, "index.html")
        else:
            # 如果用户名和密码不合法, 返回登录页面, 并提示错误信息
            context = {"error_msg": "用户名或密码不正确!", "login_form": login_form}
            return render(request, "login.html", context)
# 在前端模板中获取 form 的 errors 只需要 login_form.errors 即可, 注意 errors 前面没有下划线, 不要写成 login_form._errors
# 因为 errors 是字典, 所以可以用 .items() 来遍历, 不过要注意模板语法中调用方法不能带有括号, 所以直接写成 .items

{% for key, error in login_form.errors.items %}
    {{ error }}
{% endfor %}
# templates/login.html

            <div class="fl form-box">
                <h2>帐号登录</h2>
                <form action="{% url "login" %}" method="post" autocomplete="off">
                    <input type='hidden' name='csrfmiddlewaretoken' value='mymQDzHWl2REXIfPMg2mJaLqDfaS1sD5' />
                    <div class="form-group marb20 {% if login_form.errors.username %}errorput{% endif %}">
                        <label>用&nbsp;户&nbsp;名</label>
                        <input name="username" id="account_l" type="text" placeholder="手机号/邮箱" />
                    </div>
                    <div class="form-group marb8 {% if login_form.errors.password %}errorput{% endif %}">
                        <label>密&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;码</label>
                        <input name="password" id="password_l" type="password" placeholder="请输入您的密码" />
                    </div>
                    <div class="error btns login-form-tips" id="jsLoginTips">
                        {% for key, error in login_form.errors.items %}
                            {{ error }}
                        {% endfor %}
                        {{ error_msg }}
                    </div>
                     <div class="auto-box marb38">

                        <a class="fr" href="forgetpwd.html">忘记密码?</a>
                     </div>
                     <input class="btn btn-green" id="jsLoginBtn" type="submit" value="立即登录 > " />
                    {% csrf_token %}
                </form>
                <p class="form-p">没有慕学在线网帐号?<a href="register.html">[立即注册]</a></p>
            </div>

测试下错误信息提示

010.png 012.png 011.png
# apps/users/views.py

...
class LoginView(View):
    """
    用户登录类视图
    将上面的用户登录视图函数修改为类视图的形式, 继承 django 的 View 类,
    在这里重新定义 get、post 方法, django 会根据 request 的方法而自动调用相应方法,
    django 自动判断 request.method 为 GET 时, 会自动调用 get 方法, POST 同理.
    """
    def get(self, request):
        context = {}
        return render(request, "login.html", context)

    def post(self, request):
        # 首先实例化我们定义的 LoginForm, LoginForm 需要一个字典作为参数
        # 这里可以直接把 request.POST 传进来, request.POST 本身就是一个字典
        # 实例化后, 其实 django 已经通过 form 自动完成了 username 和 password 的验证
        login_form = LoginForm(request.POST)
        if login_form.is_valid():
            # login_form 的 is_valid() 方法, 可以判断前端提交多来的字段是否符合定义,
            # is_valid 方法实际上是判断 errors 是否为空, 如果为空, 说明没有错误,
            # 如果 errors 不为空, 返回错误信息
            user_name = request.POST.get("username", "")
            user_password = request.POST.get("password", "")
            user = authenticate(username=user_name, password=user_password)
            if user is not None:
                login(request, user)
                return render(request, "index.html")
            else:
                context = {"error_msg": "用户名或密码不正确!"}
                return render(request, "login.html", context)
        else:
            # 如果用户名和密码不合法, 返回登录页面, 并提示错误信息
            context = {"login_form": login_form}
            return render(request, "login.html", context)
014.png 015.png 016.png

探索 Django 的登录机制,了解 Django 的 user 是如何实现登录的

  1. cookie 是什么?
  • cookie 存储方式
    实际上就是浏览器支持的一种本地存储方式,它的存储方式是以 dict 键值对的方式存储的(类似 python 中字典,如{"sessionkey":"123"}),实际上 cookie 在浏览器中存储的是一个文本,浏览器会对这种文本进行解析。
  • 为什么会有 cookie 的存在
    http 协议本身是一种无状态的协议,就是说,我们的服务器在接收到浏览器的请求之后,服务器是直接返回内容给浏览器,他是不管浏览器是谁请求的,比如,用户向服务器发起第一个请求的时候,服务器直接返回,用户在发起第二个请求,这个请求和第一个请求之间没有任何联系。这就是无状态请求的一种方式。


    017.png

    这样的无状态请求,在某些情况下是没有任何问题的,比如说,浏览性的东西,A 浏览一个新闻,B 浏览一个新闻,服务器只需要把这个新闻返回给客户端就可以了,但是在某些特殊的情况下,比如说常用的网站,像淘宝,在没有登录的情况下浏览了某些东西,淘宝是给我们记住哪个用户浏览了哪些商品的,它会记住 A 浏览了哪些商品,B 浏览了哪些商品,如果我们只是用 http 无状态协议是没办法做到的。
    想要实现服务器能够记住浏览器 A,和浏览器 B,就可以用到 cookie 来实现有状态的请求,比如,浏览器 A 在给服务器发送请求后,服务器自动给浏览器生成一个 id 叫 1,然后把这个 1 返回给浏览器,浏览器 A 再把这个 id 1 放到 cookie 当中,在下一次请求的时候,浏览器就带着 id 向服务器做请求,服务器这个时候当然就知道是哪个客户端发起的请求了。
    (浏览器为了安全性,对 cookie 的存储是有要求的,比如服务器 A 传过来的 cookie 是放在服务器 A 这个域名之下的,是不能跨域访问的,这是一种安全机制。)


    018.png
  1. session 是什么?

session 实际上是存储到服务器上的,可以拿 cookie 作对比。
同样想要实现服务器能够记住浏览器 A,和浏览器 B,就可以用到 session 来实现有状态的请求,比如,浏览器 A 在给服务器发送请求后,服务器自动给浏览器生成一个 id,这个 id 既可以叫用户的 user id(比如数据库中存储的一个 id),实际上也可以是任意一段随机的字符串,这个字符串可以叫 session 的 id,(每个框架生成 session 的机制是不一样的),浏览器 A 登录注册之后,服务器根据它的用户名和密码生成 session id,然后把这个 id 返回给浏览器,浏览器 A 再把这个 session id 放到 cookie 当中,每次请求页面的时候,浏览器就带着 session id 向服务器做请求,服务器这个时候当然就知道是哪个客户端发起的请求了,并且服务器会取出 session 中用户的信息。


021.png
023.png 025.png 024.png
# mxonline/settings.py

# Application definition

INSTALLED_APPS = [
    ...
    'django.contrib.sessions',
    ...
]

二、用户注册

# apps/users/views.py

...
class RegisterView(View):
    def get(self, request):
    # 如果是 get 请求,直接返回注册页面    
    return render(request, "register.html", context={})
...
# mxonline/urls.py

from users.views import RegisterView


urlpatterns = [
    ...
    path("register/", RegisterView.as_view(), name="register"),
]
027.png

github 地址:https://github.com/mbi/django-simple-captcha
根据文档来配置:http://django-simple-captcha.readthedocs.io/en/latest/

  1. 进入项目虚拟环境中安装 django-simple-captcha
pythonic@pythonic-machine:~$ workon mxonline
(mxonline) pythonic@pythonic-machine:~$ pip install  django-simple-captcha
Collecting django-simple-captcha
  Downloading django-simple-captcha-0.5.6.zip (226kB)
    100% |████████████████████████████████| 235kB 49kB/s 
Requirement already satisfied: setuptools in ./.virtualenvs/mxonline/lib/python3.6/site-packages (from django-simple-captcha)
Collecting six>=1.2.0 (from django-simple-captcha)
  Using cached six-1.11.0-py2.py3-none-any.whl
Requirement already satisfied: Django>=1.7 in ./.virtualenvs/mxonline/lib/python3.6/site-packages (from django-simple-captcha)
Requirement already satisfied: Pillow>=2.2.2 in ./.virtualenvs/mxonline/lib/python3.6/site-packages (from django-simple-captcha)
Collecting django-ranged-response==0.2.0 (from django-simple-captcha)
  Downloading django-ranged-response-0.2.0.tar.gz
Requirement already satisfied: pytz in ./.virtualenvs/mxonline/lib/python3.6/site-packages (from Django>=1.7->django-simple-captcha)
Requirement already satisfied: olefile in ./.virtualenvs/mxonline/lib/python3.6/site-packages (from Pillow>=2.2.2->django-simple-captcha)
Building wheels for collected packages: django-simple-captcha, django-ranged-response
  Running setup.py bdist_wheel for django-simple-captcha ... done
  Stored in directory: /home/pythonic/.cache/pip/wheels/5a/c5/f9/a244926c2ee699b39f66a67205ee166104a4559e4c35357364
  Running setup.py bdist_wheel for django-ranged-response ... done
  Stored in directory: /home/pythonic/.cache/pip/wheels/e5/cc/1d/cc7d7a686d77270ab9185d3d90a63b1cd5c9e7698ab8254ff2
Successfully built django-simple-captcha django-ranged-response
Installing collected packages: six, django-ranged-response, django-simple-captcha
Successfully installed django-ranged-response-0.2.0 django-simple-captcha-0.5.6 six-1.11.0
(mxonline) pythonic@pythonic-machine:~$ 

  1. 将 captcha app 放到项目 settings.py 的 INSTALLED_APPS 当中,接下来是要通过这个 app 去生成存放图片验证码路径地址的表
# mxonline/settings.py

INSTALLED_APPS = [
    ...
    "captcha",
]

3.通过 makemigrations 和 migrate 完成数据库迁移操作

028.png
029.png 031.png
  1. 配置根级 url
from django.urls import path, include


urlpatterns = [
    ...
    path('captcha/', include('captcha.urls')),
]
  1. 在 Form 中定义图片验证码字段
# apps/users/forms.py

from django import forms
from captcha.fields import CaptchaField


class RegisterForm(forms.Form):
    """
    用户注册表单
    """
    # 要求用户通过邮箱来注册, EmailField 的字段可以验证前端传过来的字段值必须符合 email 的正则表达式
    email = forms.EmailField(required=True)
    password = forms.CharField(required=True, min_length=6, max_length=18)
    # django-simple-captcha 为我们提供了一个 captcha 字段,
    # 此字段专门用来存储并验证前端注册时填写的图片验证码,
    # 在 django Form 中, 可以通过 error_messages 来定制错误信息,
    # CaptchaField 的 error_messages key 值必须是 "invalid"
    captcha = CaptchaField(error_messages={"invalid": "验证码不正确!"})

  1. 完善 apps/users/views.py 中的 RegisterView 类视图,编写用户注册逻辑
# apps/users/views.py

from django.contrib.auth.hashers import make_password

from .forms import RegisterForm


class RegisterView(View):
    """
    用户注册类视图
    """
    def get(self, request):
        # 在返回 register.html 页面之前, 需要实例化 RegisterForm
        register_form = RegisterForm()
        # 将实例化对象 register_form 传递到前端模板中
        context = {"register_form": register_form}
        return render(request, "register.html", context)

    def post(self, request):
        # post 请求过来, 实例化 RegisterForm 时, 将 request.POST 作为参数传递进来
        register_form = RegisterForm(request.POST)
        if register_form.is_valid():
            # 获取用户注册时提交过来的表单数据
            user_name = request.POST.get("username", "")
            user_password = request.POST.get("password", "")
            
            # 将用户的数据保存到数据库
            user_profile = UserProfile()
            user_profile.username = user_name
            user_profile.email = user_name
            # 用户提交过来的密码要先进行加密, 之后在保存到数据库,
            # 而 django 已经为我们提供了 make_password 方法来对用户的明文密码进行加密,
            # 记得在顶部先引入进来, 它被放在 django.contrib.auth.hashers 中
            user_profile.password = make_password(user_password)
            user_profile.save()
  1. 在 register.html 中应用 后台传递过来的模板变量 {{ register_form }},将 {{ register_form.captcha }} 放到 验证码表单之中
# 这是 {{ register_form.captcha }} 最后会自动生成的 html,
# 图片的路径也不需要我们手动配置, 都是自动配置好的
<img src="[/captcha/image/4849697271f2af79aa616550d6dbb01b5a257d01/](http://127.0.0.1:8000/captcha/image/4849697271f2af79aa616550d6dbb01b5a257d01/)" alt="captcha" class="captcha" /><input id="id_captcha_0" name="captcha_0" type="hidden" value="4849697271f2af79aa616550d6dbb01b5a257d01" />

                <div class="tab-form">
                    <form id="email_register_form" method="post" action="{% url "register" %}" autocomplete="off">
                        <div class="form-group marb20 ">
                            <label>邮&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;箱</label>
                            <input  type="text" id="id_email" name="email" value="None" placeholder="请输入您的邮箱地址" />
                        </div>
                        <div class="form-group marb8 ">
                            <label>密&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;码</label>
                            <input type="password" id="id_password" name="password"  value="None" placeholder="请输入6-18位非中文字符密码" />
                        </div>
                        <div class="form-group marb8 captcha1 ">
                            <label>验&nbsp;证&nbsp;码</label>
                            {{ register_form.captcha }}
                        </div>
                        <div class="error btns" id="jsEmailTips"></div>
                        <div class="auto-box marb8">
                        </div>
                        <input class="btn btn-green" id="jsEmailRegBtn" type="submit" value="注册并登录" />
                    {% csrf_token %}
                    </form>
                </div>
033.png 034.png
# apps/users/models.py

class EmailVerifyRecord(models.Model):
    """
    邮箱验证码表
    """

    register = "register"
    forget = "forget"
    send_type_choices = (
        (register, "注册"),
        (forget, "找回密码"),
    )

    code = models.CharField(max_length=20, verbose_name="验证码")
    email = models.EmailField(max_length=50, verbose_name="邮箱")
    # 定义 send_type 字段,可以区分验证码的类型,如 注册验证码、找回密码验证码
    send_type = models.CharField(max_length=8, choices=send_type_choices, verbose_name="验证码类型")
    # 注意, datetime.now 不能写成 datetime.now(),
    # 有括号的话,会根据当前 model 编译的时间来生成默认时间
    # 去掉括号,是根据当前 class 实例化的时间来生成默认时间
    send_time = models.DateTimeField(default=datetime.now, verbose_name="发送时间")

    class Meta:
        verbose_name = "邮箱验证码"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s (%s)" % (self.code, self.email)
# apps/utils/email_send.py

import random

from django.core.mail import send_mail

from mxonline.settings import EMAIL_FROM
from users.models import EmailVerifyRecord


def random_str(random_length=8):
    """
    生成随机字符串函数
    """
    str = ""
    chars = "AaBbCcDdEeFfGgHhIiJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"
    length = len(chars) - 1

    for i in range(random_length):
        str += chars[random.randint(0, length)]

    return str


def send_register_email(email, send_type="register"):
    """
    定义发送邮件的基础函数, 此函数接收两个参数,
    :param email: 需要发送邮件的邮箱,
    :param send_type: 发送验证码类型, 在 EmailVerifyRecord 模型类中, "register" 代表注册, "forget" 代表找回密码
    :return:
    """
    # 在发送邮件之前, 先将要发送的内容保存到数据库中,
    # 因为要在用户点击了这个邮箱链接跳转回来的时候,
    # 我们需要查询下这个链接是否存在数据库中
    email_record = EmailVerifyRecord()
    # 通常, 我们在实现邮箱验证码的这个功能时候,
    # 会在发给用户的链接里面加一个随机字符串, 这个字符串是后台生成的,
    # 别人是没法伪造的, 在 EmailVerifyRecord 中的 code 字段就是这个随机字符串,
    # 用户注册的时候, 收到带有这个 code 随机字符串的链接,
    # 用户点击这个链接, 我们将里面的 code 取出来, 查询是否在数据库中存在,
    # 如果存在, 就将用户的账号激活, 如果不存在, 抛出相应错误给用户
    code = random_str(16)
    email_record.code = code
    email_record.email = email
    email_record.send_type = send_type
    email_record.save()

    # 向用户发送邮件, 可以通过 django 为我们提供的内部函数函数 send_email 来完成,
    # 这是 django 的方便之处, 它为我们封装好了函数, 在发送邮件之前, 我们需要定义好我们的邮件内容

    # 定义邮件标题和内容
    email_title = ""
    email_body = ""

    # 要对邮件的类型做判断, 注册邮件、找回密码邮件是不一样的
    if send_type == "register":
        # 如果是发送注册邮件, 按照以下逻辑处理
        email_title = "慕学在线网激活链接"
        email_body = "请点击下面的链接来激活你的账号:http://127.0.0.1:8000/active/%s" % code

        # 使用 send_email 来发送邮件, 我们只需要调用它, django 会根据我们定义好的配置自动发送邮件,
        # 发送之前需要去 settings.py 中配置发送者的参数信息,
        # send_mail 需要几个参数,
        # param subject: 邮件标题
        # param message: 邮件内容
        # param from_email: 可以直接调用 settings.py 中配置的 EMAIL_FROM
        # param recipient_list: 需要接收邮件的列表(也就是用户注册的邮箱, 必须是一个列表类型)
        # send_mail 函数将会返回一个布尔类型的值, True/False, 指示邮件是否发送成功
        send_status = send_mail(email_title, email_body, EMAIL_FROM, [email])
        if send_status:
            pass

# mxonline/settings.py

# 发送邮件配置参数
EMAIL_HOST = "smtp.sina.com"  # 发送邮件的服务器地址
EMAIL_PORT = 25  # 通常都是 25
EMAIL_HOST_USER = "pythonic007@sina.com"  # 邮箱登录账号
EMAIL_HOST_PASSWORD = "test123"  # 邮箱登录密码
EMAIL_USE_TLS = False  # 这个参数默认设置为 False 即可
EMAIL_FROM = "pythonic007@sina.com"  # 指明发件人, 这个参数要和 EMAIL_HOST_USER 保持一致, 不然会出错

# apps/users/views.py

...
from utils.email_send import send_register_email


class RegisterView(View):
    """
    用户注册类视图
    """
    def get(self, request):
        # 在返回 register.html 页面之前, 需要实例化 RegisterForm
        register_form = RegisterForm()
        # 将实例化对象 register_form 传递到前端模板中
        context = {"register_form": register_form}
        return render(request, "register.html", context)

    def post(self, request):
        # post 请求过来, 实例化 RegisterForm 时, 将 request.POST 作为参数传递进来
        register_form = RegisterForm(request.POST)
        if register_form.is_valid():
            # 获取用户注册时提交过来的表单数据
            user_name = request.POST.get("email", "")
            user_password = request.POST.get("password", "")

            # 将用户的数据保存到数据库
            user_profile = UserProfile()
            user_profile.username = user_name
            user_profile.email = user_name
            # 用户提交过来的密码要先进行加密, 之后再保存到数据库,
            # 而 django 已经为我们提供了 make_password 方法来对用户的明文密码进行加密,
            # 记得在顶部先引入进来, 它被放在 django.contrib.auth.hashers 中,
            # 我们只需要在调用 make_password 的时候将明文密码 user_password 当做参数传递进去就可以了
            user_profile.password = make_password(user_password)
            # 用户在注册的时候要将 is_active 设定为 False, 表明用户未激活,
            # 当用户点击我们发给用户邮箱里面的激活链接后, 再将用户改成激活状态
            user_profile.is_active = False
            user_profile.save()

            # 调用发送注册邮件的函数
            send_register_email(user_name, "register")
            # 如果用户注册成功, 重定向到登录页面
            return render(request, "login.html")
        else:
            # 如果用户注册失败, 返回注册页, 并且显示错误信息
            return render(request, "register.html", context={"register_form": register_form})
039.png 040.png 041.png 042.png
# {{ register_form.email.value }} 值即为上次用户提交的表单数据
<input  type="text" id="id_email" name="email" value="{{ register_form.email.value }}" placeholder="请输入您的邮箱地址" />
# templates/register.html

...
<div class="tab-form">
    <form id="email_register_form" method="post" action="{% url "register" %}" autocomplete="off">
        <div class="form-group marb20 {% if register_form.errors.email %}errorput{% endif %}">
            <label>邮&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;箱</label>
            <input  type="text" id="id_email" name="email" value="{{ register_form.email.value }}" placeholder="请输入您的邮箱地址" />
        </div>
        <div class="form-group marb8 {% if register_form.errors.password %}errorput{% endif %}">
            <label>密&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;码</label>
            <input type="password" id="id_password" name="password"  value="{{ register_form.password.value }}" placeholder="请输入6-20位非中文字符密码" />
        </div>
        <div class="form-group marb8 captcha1 {% if register_form.errors.captcha %}errorput{% endif %}">
            <label>验&nbsp;证&nbsp;码</label>
            {{ register_form.captcha }}
{#                            <img src="/captcha/image/2f3f82e5f7a054bf5caa93b9b0bb6cc308fb7011/" alt="captcha" class="captcha" /> <input id="id_captcha_0" name="captcha_0" type="hidden" value="2f3f82e5f7a054bf5caa93b9b0bb6cc308fb7011" /> <input autocomplete="off" id="id_captcha_1" name="captcha_1" type="text" />#}
        </div>
        <div class="error btns" id="jsEmailTips">
            {% for key,error in register_form.errors.items %}
                {{ error }}
            {% endfor %}

        </div>
        <div class="auto-box marb8">
        </div>
        <input class="btn btn-green" id="jsEmailRegBtn" type="submit" value="注册并登录" />
    {% csrf_token %}
    </form>
</div>
...
# mxonline/urls.py

from django.urls import path, re_path

from users.views import ActiveUserView


urlpatterns = [
    ...
    re_path(r"^active/(?P<active_code>[a-zA-Z0-9]{16})/$", ActiveUserView.as_view(), name="user_active"),
]
# apps/users/views.py

class ActiveUserView(View):
    """
    激活用户视图
    """

    # 处理激活用户只需要覆写父类的 get 方法即可, 并且将在 url 中提取出来的参数 active_code 传递进来
    def get(self, request, active_code):
        # 先取出全部符合条件的验证码, 因为可能有多条数据, 所以不能用 get方法, 选择用 filter 方法
        all_records = EmailVerifyRecord.objects.filter(code=active_code)
        if all_records:
            for record in all_records:
                email = record.email
                user = UserProfile.objects.get(email=email)
                user.is_active = True
                user.save()
        # 激活成功, 跳转登录页
        return render(request, "login.html")

# apps/users/views.py

...
class LoginView(View):
    """
    用户登录类视图
    将上面的用户登录视图函数修改为类视图的形式, 继承 django 的 View 类,
    在这里重新定义 get、post 方法, django 会根据 request 的方法而自动调用相应方法,
    django 自动判断 request.method 为 GET 时, 会自动调用 get 方法, POST 同理.
    """
    def get(self, request):
        context = {}
        return render(request, "login.html", context)

    def post(self, request):
        # 首先实例化我们定义的 LoginForm, LoginForm 需要一个字典作为参数
        # 这里可以直接把 request.POST 传进来, request.POST 本身就是一个字典
        # 实例化后, 其实 django 已经通过 form 自动完成了 username 和 password 的验证
        login_form = LoginForm(request.POST)
        if login_form.is_valid():
            # login_form 的 is_valid() 方法, 可以判断前端提交过来的字段是否符合定义,
            # is_valid 方法实际上是判断 errors 是否为空, 如果为空, 说明没有错误,
            # 如果 errors 不为空, 返回错误信息
            user_name = request.POST.get("username", "")
            user_password = request.POST.get("password", "")
            user = authenticate(username=user_name, password=user_password)
            # 判断用户是否合法
            if user is not None:
                # 判断用户是否处于激活状态
                if user.is_active:
                    login(request, user)
                    return render(request, "index.html")
                else:
                    context = {"error_msg": "用户未激活!"}
                    return render(request, "login.html", context)

            else:
                context = {"error_msg": "用户名或密码不正确!"}
                return render(request, "login.html", context)
        else:
            # 如果用户名和密码不合法, 返回登录页面, 并提示错误信息
            context = {"login_form": login_form}
            return render(request, "login.html", context)

044.png 045.png 046.png 047.png 048.png 049.png
# apps/users/views.py

...
class RegisterView(View):
    """
    用户注册类视图
    """
    def get(self, request):
        # 在返回 register.html 页面之前, 需要实例化 RegisterForm
        register_form = RegisterForm()
        # 将实例化对象 register_form 传递到前端模板中
        context = {"register_form": register_form}
        return render(request, "register.html", context)

    def post(self, request):
        # post 请求过来, 实例化 RegisterForm 时, 将 request.POST 作为参数传递进来
        register_form = RegisterForm(request.POST)
        if register_form.is_valid():
            # 获取用户注册时提交过来的表单数据
            user_name = request.POST.get("email", "")

            # 判断用户是否已经存在
            if UserProfile.objects.filter(email=user_name):
                # 如果用户已经存在, 返回注册页面和错误信息
                context = {"register_form": register_form, "error_msg": "该用户已经注册!"}
                return render(request, "register.html", context)
            user_password = request.POST.get("password", "")

            # 将用户的数据保存到数据库
            user_profile = UserProfile()
            user_profile.username = user_name
            user_profile.email = user_name
            # 用户提交过来的密码要先进行加密, 之后再保存到数据库,
            # 而 django 已经为我们提供了 make_password 方法来对用户的明文密码进行加密,
            # 记得在顶部先引入进来, 它被放在 django.contrib.auth.hashers 中,
            # 我们只需要在调用 make_password 的时候将明文密码 user_password 当做参数传递进去就可以了
            user_profile.password = make_password(user_password)
            # 用户在注册的时候要将 is_active 设定为 False, 表明用户未激活,
            # 当用户点击我们发给用户邮箱里面的激活链接后, 再将用户改成激活状态
            user_profile.is_active = False
            user_profile.save()

            # 调用发送注册邮件的函数
            send_register_email(user_name, "register")
            # 如果用户注册成功, 重定向到登录页面
            return render(request, "login.html")
        else:
            # 如果用户注册失败, 返回注册页, 并且显示错误信息
            return render(request, "register.html", context={"register_form": register_form})

...
<div class="error btns" id="jsEmailTips">
    {% for key,error in register_form.errors.items %}
        {{ error }}
    {% endfor %}
    {{ error_msg }}
</div>
...
050.png 051.png
# apps/users/views.py

...
class ActiveUserView(View):
    """
    激活用户视图
    """

    # 处理激活用户只需要覆写父类的 get 方法即可, 并且将在 url 中提取出来的参数 active_code 传递进来
    def get(self, request, active_code):
        # 先取出全部符合条件的验证码, 因为可能有多条数据, 所以不能用 get方法, 选择用 filter 方法
        all_records = EmailVerifyRecord.objects.filter(code=active_code)
        if all_records:
            for record in all_records:
                email = record.email
                user = UserProfile.objects.get(email=email)
                user.is_active = True
                user.save()
        else:
            return render(request, "active_fail.html")
        # 激活成功, 跳转登录页
        return render(request, "login.html")

053.png

三、找回密码

054.png
# apps/users/views.py
...
class ForgetPwdView(View):
    """
    找回密码类视图
    """
    def get(self, request):
        return render(request, "forgetpwd.html")

# mxonline/urls.py

...
from users.views import ForgetPwdView


urlpatterns = [
    ...
    path("forget/", ForgetPwdView.as_view(), name="forget_pwd"),
]

055.png
# apps/users/forms.py

class ForgetForm(forms.Form):
    """
    用户找回密码表单
    """
    email = forms.EmailField(required=True)
    captcha = CaptchaField(error_messages={"invalid": "验证码不正确!"})

# apps/users/views.py

...
from .forms import ForgetForm


class ForgetPwdView(View):
    """
    找回密码类视图
    """
    def get(self, request):
        forget_form = ForgetForm()
        return render(request, "forgetpwd.html", context={"forget_form": forget_form})


<div class="fl form-box">
    <h2>忘记密码</h2>
    <form id="jsFindPwdForm" method="post" action="{% url "forget_pwd" %}" autocomplete="off">
        {% csrf_token %}
        <div class="form-group marb20 ">
            <label>帐&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;号</label>
            <input type="text" id="account" name="email" value="None" placeholder="邮箱" />
        </div>
        <div class="form-group captcha1 marb38">
            <label>验&nbsp;证&nbsp;码</label>
            {{ forget_form.captcha }}
        </div>
        <div class="error btns" id="jsForgetTips"></div>
        <input type="hidden" name="sms_type" value="1">
        <input class="btn btn-green" id="jsFindPwdBtn" type="submit" value="提交" />
        <p class="form-p" style="bottom:40px;">您还可以<a href="login.html"> [直接登录]</a></p>
    
    </form>
</div>

057.png
# apps/users/views.py

...
class ForgetPwdView(View):
    """
    找回密码类视图
    """
    def get(self, request):
        forget_form = ForgetForm()
        return render(request, "forgetpwd.html", context={"forget_form": forget_form})

    def post(self, request):
        forget_form = ForgetForm(request.POST)

        if forget_form.is_valid():
            email = request.POST.get("email", "")
            # 调用发送邮件的函数, 这里要发送的类型是 forget, 即找回密码
            send_register_email(email, "forget")
            return render(request, "send_success.html")
        else:
            return render(request, "forgetpwd.html", context={"forget_form": forget_form})


def send_register_email(email, send_type="register"):
    """
    定义发送邮件的基础函数, 此函数接收两个参数,
    :param email: 需要发送邮件的邮箱,
    :param send_type: 发送验证码类型, 在 EmailVerifyRecord 模型类中, "register" 代表注册, "forget" 代表找回密码
    :return:
    """
    ...

    # 要对邮件的类型做判断, 注册邮件、找回密码邮件是不一样的
    if send_type == "register":
        ...
    elif send_type == "forget":
        # 如果是发送找回密码邮件, 按照以下逻辑处理
        email_title = "慕学在线网密码重置链接"
        email_body = "请点击下面的链接来重置你的密码:http://127.0.0.1:8000/reset/%s" % code

        send_status = send_mail(email_title, email_body, EMAIL_FROM, [email])
        if send_status:
            pass
...
058.png 059.png 060.png 061.png
# templates/forgetpwd.html

<div class="fl form-box">
    <h2>忘记密码</h2>
    <form id="jsFindPwdForm" method="post" action="{% url "forget_pwd" %}" autocomplete="off">
        {% csrf_token %}
        <div class="form-group marb20 {% if forget_form.errors.email %}errorput{% endif %}">
            <label>帐&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;号</label>
            <input type="text" id="account" name="email" value="{{ forget_form.email.value }}" placeholder="邮箱" />
        </div>
        <div class="form-group captcha1 marb38 {% if forget_form.errors.captcha %}errorput{% endif %}">
            <label>验&nbsp;证&nbsp;码</label>
            {{ forget_form.captcha }}
        </div>
        <div class="error btns" id="jsForgetTips">
            {% for key,error in forget_form.errors.items %}
                {{ error }}
            {% endfor %}

        </div>
        <input type="hidden" name="sms_type" value="1">
        <input class="btn btn-green" id="jsFindPwdBtn" type="submit" value="提交" />
        <p class="form-p" style="bottom:40px;">您还可以<a href="login.html"> [直接登录]</a></p>

    </form>
</div>

062.png
# apps/users/views.py

class ResetPwdView(View):
    """
    重置密码类视图
    """

    # 处理重置密码只需要覆写父类的 get 方法即可, 并且将在 url 中提取出来的参数 active_code 传递进来
    def get(self, request, active_code):
        # 先取出全部符合条件的验证码, 因为可能有多条数据, 所以不能用 get方法, 选择用 filter 方法
        all_records = EmailVerifyRecord.objects.filter(code=active_code)
        if all_records:
            for record in all_records:
                email = record.email
                # 如果查找到符合条件的用户邮箱, 返回重置密码页面,
                # 并且将 email 也传递到前端页面, 因为用户在重置密码页面的时候,
                # 我们不知道是哪个用户在重置密码, 所以这里要将用户的 email 放到 form 表单中,
                # 当用户输入新密码后提交表单, 这个 email 会随着 form 表单传递回来,
                # 我们在后台接收一下, 就能知道是哪个用户在重置密码了,
                # 前端页面可以将这个 email 放到 hidden 类型的 input 标签中,
                # 用户是看不到的, 但他会跟随 form 表单提交进来
                return render(request, "password_reset.html", context={"email": email})
                ...

# templates/password_reset.html

<div class="resetpassword" id="resetPwdForm">
    <h1>修改密码</h1>
    <p>已经通过验证,请设置新密码</p>
    <form id="reset_password_form" action="" method="post">
        <ul>
            <li>
                <span class="">新 密 码 :</span>
                <input type="password" name="password" id="pwd" placeholder="6-20位非中文字符">
                <i></i>
            </li>
            <li>
                <span class="">确定密码:</span>
                <input type="password" name="password2" id="repwd" placeholder="6-20位非中文字符">
                <i></i>
            </li>
            <input type="hidden" value="{{ email }}">
            <li class="button">
                <input type="button" value="提交" onclick="reset_password_form_submit()">
            </li>
        </ul>
    </form>
</div>

# mxonline/urls.py

from users.views import ResetPwdView


urlpatterns = [
    ...
    re_path(r"^reset/(?P<active_code>[a-zA-Z0-9]{16})/$", ResetPwdView.as_view(), name="reset_pwd"),
]

063.png 064.png
# apps/users/forms.py

class ModifyPwdForm(forms.Form):
    """
    用户重置密码表单
    """
    # 因为前端页面中用户需要输入两次密码, 所以这里要定义两个密码字段
    password1 = forms.CharField(required=True, min_length=6, max_length=18)
    password2 = forms.CharField(required=True, min_length=6, max_length=18)

# apps/users/views.py

...
from .forms import ModifyPwdForm


class ModifyPwdView(View):
    """
    重置密码 post 请求类视图, 因为 ResetPwdView 视图的 url 需要一个参数,
    而 post 请求时是没有参数的, 所以不能将 post 请求放到 ResetPwdView 类视图中,
    所以还要为此视图配置一个单独的 url
    """
    def post(self, request):
        modify_pwd_form = ModifyPwdForm(request.POST)
        if modify_pwd_form.is_valid():
            # 如果 form 验证成功, 取出用户输入的两次密码和邮箱
            password1 = request.POST.get("password1", "")
            password2 = request.POST.get("password2", "")
            email = request.POST.get("email", "")

            # 如果两次输入密码不一致, 返回此页面和错误信息
            if password1 != password2:
                context = {"email": email, "error_msg": "两次输入密码不一致!"}
                return render(request, "password_reset.html", context)

            # 如果两次输入密码相同, 保存用户新密码到数据库, 并返回登录页
            user = UserProfile.objects.get(email=email)
            user.password = make_password(password1)
            user.save()
            return render(request, "login.html")
        else:
            # 如果 form 验证失败, 返回此页面和错误信息
            email = request.POST.get("email", "")
            context = {"email": email, "modify_pwd_form": modify_pwd_form}
            return render(request, "password_reset.html", context)

# templates/password_reset.html

<div class="resetpassword" id="resetPwdForm">
    <h1>修改密码</h1>
    <p>已经通过验证,请设置新密码</p>
    <form id="reset_password_form" action="{% url "modify_pwd" %}" method="post">
        {% csrf_token %}
        <ul>
            <li>
                <span class="{% if modify_pwd_form.errors.password1 %}errorput{% endif %}">新 密 码 :</span>
                <input type="password" name="password1" id="pwd" placeholder="6-18位非中文字符">
                <i></i>
            </li>
            <li>
                <span class="{% if modify_pwd_form.errors.password2 %}errorput{% endif %}">确定密码:</span>
                <input type="password" name="password2" id="repwd" placeholder="6-18位非中文字符">
                <i></i>
            </li>
            <input type="hidden" name="email" value="{{ email }}">
            <div class="error btns">
                {% for key, error in modify_pwd_form.errors.items %}
                    {{ error }}
                {% endfor %}
                {{ error_msg }}
            </div>
            <li class="button">
                <input type="submit" value="提交">
            </li>
        </ul>
    </form>
</div>

# mxonline/urls.py

from users.views import ModifyPwdView


urlpatterns = [
    ...
    path("modify_pwd", ModifyPwdView.as_view(), name="modify_pwd"),
]

065.png 066.png 067.png 068.png
上一篇 下一篇

猜你喜欢

热点阅读