美文网首页
Django 开发 MxOnline 项目笔记 -- 第6章 用

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

作者: 江湖十年 | 来源:发表于2018-03-05 17:45 被阅读1113次

    一、用户登录

    • 项目根目录下创建 static/ 目录,将静态文件放进来(如:.css/.js/图片)
    • 将 index.html 和 login.html 放到 templates/ 目录下


      001.png
    • 在 index.html 和 login.html 第一行写上下面这句话,以此来加载静态文件,这是 django 的模板语法,要加载静态文件,html 文件最上面要写上它。
    {% load staticfiles %}
    
    • 替换 index.html 和 login.html 中全部静态文件的引用
    <link rel="stylesheet" type="text/css" href="../css/reset.css">
    
    • 将类似上面这种引用全部替换成下面这种
    <link rel="stylesheet" type="text/css" href="{% static "css/reset.css" %}">
    
    • 在不编写 django view 视图函数的情况下,可以直接访问前端静态页面,只需要利用 django 自带的类视图 TemplateView 即可完成。
    # 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"),
    ]
    
    • 现在浏览器地址栏输入 http://127.0.0.1:8000/ 即可访问首页。


      002.png
    • 利用 django 自带的 authenticate 和 login 来完成用户验证登录。
    • authenticate 方法用来验证用户名、密码是否合法
    • 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)
    
    • 配置 url
    # 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"),
    ]
    
    • 编辑 index.html 文件,通过判断用户是否登录,来判断前端页面应该显示用户头像还是登录按钮
    • 在前台模板中判断用户是否登录可以用 user 对象的 is_authenticated 方法
    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
    • 输入用户名密码 回车登录(这里使用项目的后台超级用户登录)


      004.png
    • 可以看到首页的 登录按钮 换成了 用户头像信息

    • 需要注意:用户登录后,django 已经通过 session 记录了用户的登录状态,在一段时间内,只要访问网站就是登录状态,如果想在不清除浏览器缓存的情况下退出用户登录状态,可以通过直接退出 admin 后台账户的登录状态即可同步退出前端页面的用户登录状态

    扩展用户登录
    • 想使系统不仅可以通过用户名来登录,也可以同时使用邮箱来登录

    • django 给我们提供了一种可以自定义后台登录认证的方法,即我们可以自己来定义用户登录时要验证的登录方式

    • step1:配置 settings.py 文件,重载 AUTHENTICATION_BACKENDS 变量。

    # mxonline/settings.py
    
    # 重载项目的 AUTHENTICATION_BACKENDS 变量
    AUTHENTICATION_BACKENDS = [
        "users.views.CustomBackend"
    ]
    
    • step2:在 users/views.py 中 自定义 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
    ...
    
    将登录视图函数修改成类视图
    • 将视图函数修改成类视图要继承 django 的某个类视图,编辑 apps/users/views.py 文件。
    # 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")
    
    ...
    
    • 配置 url
    • 因为 url 配置这里只能调用视图函数,而不能直接调用类,所以用 as_view() 方法将类视图转换成视图函数。
    # mxonline/urls.py
    
    ...
    from users.views import LoginView
    
    
    urlpatterns = [
        ...
        path("login/", LoginView.as_view(), name="login"),
    ]
    
    利用 django form 表单验证用户登录
    • 比如要判断表单是否为必填字段,验证表单长度等
    • step1:在 apps/users/ 目录下新建 forms.py 文件,用来存储 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)
    
    • step2:修改 apps/users/views.py 中用户登录视图逻辑
    # 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)
    
    • step3: 现在可以前端页面进行登录验证下 view 中逻辑是否正确,看下 form 验证能否达到预期效果
      1. 验证不输入用户名


        006.png

        可以看到报错信息


        007.png
      2. 验证输入不合法的验证码


        008.png

        可以看到报错信息


        009.png
        可以看到, 在不需要查询数据库的前提下,已经可以通过 form 过滤一些错误信息了。
    • step4:将 form 验证的错误信息传递到前端
    # 在前端模板中获取 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
    • 编辑 LoginView 类视图,完善错误信息提示
    # 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)
    
    • 再次测试错误信息提示


      013.png
    014.png 015.png 016.png

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

    • cookie 和 session 在 django 中的应用
    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
    • 在浏览器中查看 cookie


      019.png
    • 通过使用 cookie 就可以完成一些用户信息的保存,比如说服务器给浏览器返回了 user 的信息。

    • 通过本地项目网址来查看下
      为了不每次访问网站的时候都去登录,我们可以采用一种模式就是,每次像服务器请求的时候,都会带上 cookie 当中的用户名和密码,比如在登录的时候,可以把用户名和密码全部保存到 cookie 当中来,比如设置为 {"username": "pythonic", "password": "123456"},然后把这些 cookie 数据发送给后台服务器,后台服务器接收到时,对用户名和密码做验证登录。这样是可以使用 cookie 完成自动登录功能的,每次不需要手动输入用户名密码手动完成登录,而是在浏览器发送请求时自动携带 cookie 信息到服务器,来完成自动登录。


      020.png
    • 这种方式实际上是 cookie 的一种最原始的存储机制,把用户信息放在了浏览器本地,但这样的话就引申出一个安全问题,比如某个人拿到用户电脑之后,通过 cookie 就可以分析出用户的敏感信息,用户名、密码等放在 cookie 中的所有东西,实际上都是可以拿出来的,这就是 cookie 的一种隐患,为了解决这个隐患,就引申出来了 session。

    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
    • 实际上登录的整个过程已经非常清晰了,用户在输入用户名和密码之后, 根据后台逻辑,调用 django 的 login 方法,它根据用户的信息,生成了一个 session id,这个 session id 是必须要保存到数据库当中的,因为用户登录之后,他需要查询这个 session 取出用户的基本信息。
    • 看一下 项目数据库 django 为我们自动生成的数据表


      022.png
    023.png
    • 这个表实际上就是存储 django 为每个用户生成的 session 信息,比如 session id,实际上 django 在存储用户信息的时候是会对用户信息进行加密的,生成一个 session_data。
    • 可以在前端页面通过用户名密码登录后,查询数据库,即可看到 django 自动为我们生成了一条新记录,记录涉及 3 个字段,session_key 实际上就是前面说到的服务器给用户返回的 session id,可以发现在浏览器中这个值是保存为 sessionid 的,session_data 是一段加密的文字,是把用户的信息(比如名称、密码和其他字段信息)生成一段字符串做过加密的,expire_time 是过期时间,我们可以在 django 项目中设置过期时间的,django 就好根据我们的设置自动生成过期时间。
    025.png 024.png
    • 理解 session 到底是 django 哪一部分做成的,浏览器请求服务器携带 sessionid,django 是怎么把这个 sessionid 给转换成 user 的,其实是在 settings.py 中有一个 app 的配置,'django.contrib.sessions',这个 app 是会对每一个 request 和 response 做拦截的,拦截到浏览器请求过来的 request 的时候,会找到其中带过来的 sessionid,之后根据这个 sessionid 去数据库表做查询,查询到了,就说明有这个 user,之后做解密,把 session_data 取出来(内部带有用户信息)。在 返回 response 的时候,它也会主动加上 sessionid。
    # mxonline/settings.py
    
    # Application definition
    
    INSTALLED_APPS = [
        ...
        'django.contrib.sessions',
        ...
    ]
    
    • 总结 cookie 和 session 区别:
      cookie 是浏览器的一种本地存储机制,是本地的状态,存储在浏览器当中的,与服务器是没有关系的,他可以存储很多信息,如用户名、密码、以及服务器返回的任何信息。cookie 是浏览器(本地)的一种行为,可以在本地存储任何键值对,但这个键值对是存储在某个域名之下的,每个域名之下的 键值对是不可以互相访问的。用户信息都存储在本地是不安全的。
      session 是服务器生成的,是存储在服务器端的,如服务器根据用户名密码生成一段带有过期时间的随机字符串,服务器把 session 发给用户,用户将收到的 session 存储到 cookie 当中,用户下次请求的时候通过 cookie 将 session 信息带回服务器,服务器通过 sessionid 查询数据库找到对应用户,之后对用户标记,这样 django 就通过 session 和 cookie 机制完成了用户自动登录机制。

    二、用户注册

    • step1:将前端页面 register.html 复制到 templates/ 目录下
      • 修改 index.html 登录按钮跳转链接,修改 register.html 中静态文件的引入


        026.png
    • step2:在 users/views.py 中定义类视图 RegisterView() 用来实现注册逻辑
    # apps/users/views.py
    
    ...
    class RegisterView(View):
        def get(self, request):
        # 如果是 get 请求,直接返回注册页面    
        return render(request, "register.html", context={})
    ...
    
    • step3:配置注册页面的 url
    # mxonline/urls.py
    
    from users.views import RegisterView
    
    
    urlpatterns = [
        ...
        path("register/", RegisterView.as_view(), name="register"),
    ]
    
    027.png
    • 通过注册页面可以发现,注册是通过邮箱来完成的,并且需要输入图片验证码来完成。
    • step5:学习使用专门用来做 django 验证码的一个开发库 django captcha

    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
    • 数据库中可以查看生成的数据表


      030.png
    031.png
    1. 配置根级 url
    from django.urls import path, include
    
    
    urlpatterns = [
        ...
        path('captcha/', include('captcha.urls')),
    ]
    
    • 至此,图片验证码的基本配置已经完成,下面来应用这些配置
    1. 在 Form 中定义图片验证码字段
    • 在 apps/users/forms.py 文件中,定义 RegisterForm(),把图片验证码字段添加进来
    # 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 }} 放到 验证码表单之中
    • 实际上在 django 的 Form 中,EmailField、CharField 这些字段在生成字符串的时候,是会生成一段 html 代码的(如 input 输入框), 可以供前端模板使用,而 {{ 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>
    
    • 进入 register.html 页面,查看网页源代码,查询数据库,可以发现这里的图片验证码都是相互对应的。
    • 验证码有一串随机字符串,如:"4849697271f2af79aa616550d6dbb01b5a257d01",在用户前端页面提交表单,传过来验证码,后台验证是否正确。
    • 注意,注册页面点击图片验证码是会自动刷新图片验证码的,但这个功能是前端 js 写的,并不是 captcha 库自带的。


      032.png
    033.png 034.png
    • step6:实现用户在通过邮箱注册的时候,自动发送一条 “激活链接” 到用户的邮箱当中

    • 因为下面会用到 apps/users/models.py 中定义的 EmailVerifyRecord 模型类,所以先回顾下这个模型

    # 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 package,在 utils/ 目录下新建一个 email_send.py 文件,用于存放发送邮件的函数

    • 目录结构如下


      037.png
    • email_send.py 中发送邮件逻辑如下

    # 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
    
    
    • 在 settings.py 中,需要配置的参数
    # 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 保持一致, 不然会出错
    
    
    • 其中 EMAIL_HOST 参数指定发送邮件的服务器地址,根据邮箱不同可以去邮箱官网找到到,需开启 SMTP 服务,以新浪邮箱为例


      038.png
    • 测试邮箱验证码发送是否可以,在 apps/users/views.py 的 RegisterView 类视图中调用 send_register_email 这个函数

    # 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
    • 通过查看 RegisterView 类视图,来查看前端页面提交过来的信息
    040.png
    • 登录邮箱查看邮件,可以成功收到注册激活链接的邮件
    041.png
    • 查看数据库中保存的用户信息
    042.png
    • 编辑 register.html,利用后台传递过来的模板变量 {{register_form}} 填充 form 信息
    • 这里要注意一点,就是当用户填写完表单提交注册以后,我们要把用户填写的注册信息回填回去,即使用户填写的表单信息有错,也要进行回填,省去了用户每次都要重新填值的麻烦,实现方法很简单,django 为我们提过了一个 value 函数,可以直接在模板中使用,只需在需要显示用户填写信息的表单标签的 value 属性中调用这个 value 函数,如下
    # {{ 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>
    ...
    
    • 测试错误信息


      043.png
    • 处理用户激活,通过用户点击邮箱中的激活链接,完成用户注册激活

    • 1.配置激活用户的 url,可以直接在 url 中提取我们需要的变量(发给用户的随机字符串)

    # 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"),
    ]
    
    • 2.定义 ActiveUserView 类视图,用于激活用户
    # 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")
    
    
    • 3.因为定义了处理用户激活的类,所以在用户登录的时候也要加上相应逻辑,判断用户是否处于激活状态,完善 LoginView 类视图
    # 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)
    
    
    • 4.测试用户在非激活状态下的登录
    044.png 045.png 046.png
    • 6.查看数据库中用户激活状态,发现该用户已经被激活了
    047.png
    • 7.再次登录该账号发现已经可以正常登录了
    048.png 049.png
    • 解决用户注册过程中遗留的 2 个问题
    • 1.在用户注册的过程中,并没有判断用户的 email 是否已经注册,所以要在 RegisterView 中加上判断用户输入的邮箱是否存在的逻辑
    # 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})
    
    
    • 编辑 templates/register.html,接收并显示后台传递过来的错误信息 {{ error_msg }}
    ...
    <div class="error btns" id="jsEmailTips">
        {% for key,error in register_form.errors.items %}
            {{ error }}
        {% endfor %}
        {{ error_msg }}
    </div>
    ...
    
    • 访问注册页面,输入已经注册过了的账号进行注册测试
    050.png 051.png
    • 2.之前的 ActiveUserView 类视图中只处理了用户链接中随机字符串存在于数据库中的情况,并没有判断如果不存在如何处理,接下来完善此视图
    # 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")
    
    
    • 定义一个简单的模板 active_fail.html 用来处理用户输入的链接在数据库中不存在的情况


      052.png
    • 输入不正确的地址进行测试

    053.png

    三、找回密码

    • 找回密码的功能需要 3 个新的页面,一个是点击找回密码按钮跳转过去的,这个页面可以输入用户邮箱的,forgetpwd.html;另一个页面是用户在
      forgetpwd.html 页面输入邮箱后提交表单,验证码发送成功后需要返回给用户的一个页面,send_success.html 用来提示用户验证码发送成功;最后还需要一个用户设置新密码的页面 password_reset.html。
    • step1:经典的 Django 三步曲,将 forgetpwd.html 模板放到 templates/ 目录下,在 apps/users/views.py 中定义 ForgetPwdView 类视图用来处理用户找回密码的逻辑,配置 forgetpwd.html 页面的 url。
    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
    • 配置 login.html 中“忘记密码”按钮的跳转链接,修改 forgetpwd.html 文件中静态文件的引入,重新打开页面


      056.png
    • step2:由于忘记密码提交表单数据时也是需要输入图片验证码的,所以这里同之前的注册页面一样,需要配置下图片验证码

    • 1.在 apps/users/forms.py 中定义 ForgetForm 表单类

    # apps/users/forms.py
    
    class ForgetForm(forms.Form):
        """
        用户找回密码表单
        """
        email = forms.EmailField(required=True)
        captcha = CaptchaField(error_messages={"invalid": "验证码不正确!"})
    
    
    • 2.在 ForgetPwdView 中实例化 ForgetForm,并将 forget_form 实例传递到模板
    # 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})
    
    
    • 3.在 forgetpwd.html 中接收模板变量 {{ forget_form }} 显示验证码,以及修改 <form> 标签的 action 属性跳转地址
    
    <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>
    
    
      1. 再次访问找回密码页面,可以看到图片验证码已经显示出来了
    057.png
    • step3:编辑 ForgetPwdView 完善 post 请求时的后台逻辑
    # 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})
    
    
    • step4:完善发送邮件函数 send_register_email,添加 send_type == "forget" 的逻辑,用来处理发送找回密码验证码
    
    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
    ...
    
    • step5:配置一个简单的邮件发送成功页面 send_success.html
    058.png 059.png 060.png 061.png
    • step7:编辑 forgetpwd.html 页面中的 form 表单部分,将后台传递过来的
      {{ forget_form }} 加入到表单代码中
    # 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
    • step8:定义 ResetPwdView 类视图用来处理用户重置密码逻辑
    # 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})
                    ...
    
    
    • step9: 将 password_reset.html 页面拷贝到 templates/ 目录下 ,并进行修改,form 表单中加一个 <input type="hidden" value="{{ 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>
    
    
    • step10.配置 ResetPwdView 类视图的 url,也就是发送到用户邮箱的密码重置链接的 url
    # 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"),
    ]
    
    
    • step11:访问发送到用户邮箱的重置密码链接,可以成功跳转到重置密码页面,查看网页源码,可以看到新加的 <input type="hidden"> 标签,这个标签前端页中是看不到的
    063.png 064.png
    • step12:接下来是处理 post 请求逻辑,也就是在用户输入新密码点击提交按钮之后的逻辑
    • 1.先在 forms.py 中定义一个新的 form 表单类 ModifyPwdForm,用来验证 password_reset.html 页面中 form 表单提交过来的数据有效性
    # 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)
    
    
    • 2.编写用户点击提交后的 post 逻辑,这里因为不能和 get 请求共用一个类视图,所以增加一个处理 post 请求类视图 ModifyPwdView
    # 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)
    
    
    • 3.编辑 password_reset.html 页面,完善 form 表单部分代码,注意:
      为 <input type="hidden" name="email" value="{{ email }}"> 标签增加了一个 name 属性,因为如果 input 标签没有 name 属性,后台 request.POST.get("email", "") 的时候是没法取值的,并且前后台的名称必须相同,password1 和 password2 同理。
    # 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>
    
    
    • 4.配置 ModifyPwdView 的 url
    # mxonline/urls.py
    
    from users.views import ModifyPwdView
    
    
    urlpatterns = [
        ...
        path("modify_pwd", ModifyPwdView.as_view(), name="modify_pwd"),
    ]
    
    
    • 5.访问发送到用户邮箱的重置密码链接,测试重置用户密码
    065.png 066.png 067.png 068.png
    • 以上,用户找回密码基本功能已经完成,还可以继续完善,比如:在 EmailVerifyRecord 中可以增加一个布尔类型字段用来标示用户的找回密码链接是否已经使用过,在 user.save() 后可以将这个 布尔类型字段值改成 True,来标示这个链接已经被用来修改过一次密码;也可以在 EmailVerifyRecord 中增加一个链接的过期时间字段,用来管理发送给用户的修改密码链接的有效时间。

    相关文章

      网友评论

          本文标题:Django 开发 MxOnline 项目笔记 -- 第6章 用

          本文链接:https://www.haomeiwen.com/subject/uhmagxtx.html