我的网站搭建(第21天) 找回密码

  • 发布时间:2016年6月30日 09:07
  • 作者:杨仕航
* 该文是基于Python2.7开发的,最新Python3.x和Django2.x视频教程可以前往 >> Django2.0视频教程

前些天有网友找我问密码,他忘记了,问我有没有找回密码的功能。

好吧,还没有...我让他提供一个新的密码(知道哪个邮箱)先给他改一下。进入服务器后台,找到Django网站目录。依次输入下面的代码($表示是Linux命令行,>>>表示Python命令行):

$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> user = User.objects.get(email = 'xxxxx@xx.com'
>>> user.set_password('xxxxxx')
>>> user.save()

通过这句话代码,就给他重置了密码。但总不能每次都找上我,然后进入后台重置一下密码吧。

好吧,是时候写找回密码的功能了。


首先,分析一下这个功能怎么构架。初步想了一下,有3种方式:

1、发送密码到注册用的邮箱;

2、发送一个重置密码的链接到邮箱;

3、发送一个验证码到邮箱,然后用户填写新密码可以直接修改。


由于Django的密码是被加密和加盐的。直接得到明文的密码比较困难。第2种和第3种方案其实没多大区别。由于第3种开发步骤和使用环节比较少。所以采用第3种方案。


接着,再进一步想一个详细的方案。

发送一个验证码到注册使用的邮箱是为了确认用户的身份。用户就可以用这个验证码去修改密码。那么就需要一个地方存储一下这个验证码,以便后台确认修改密码的时候,检查验证码是否正确。

保存验证码有两种方式,一种是sessions,一种是保存到数据库中。sessions不是很安全,验证码要求的保密程度比较高。所以还是创建一个模型和用户表关联,用于存储生成的验证码。


大致流程是:

1、在重置密码的页面,用户填写了邮箱和新密码之后。

2、点击“获取验证码”的按钮,就发送验证码到填写的邮箱中。

3、用户收到验证码之后,再复制填回重置密码的页面中。确定提交即可。


看看具体实现的代码。注意:我网站的用户认证系统是Django自身的用户。

创建应用user_ex(这个我不用python manage.py startapp user_ex这种方式创建,因为前面开发的时候有这个文件夹。里面的文档结构都是按照app的结构来处理的。所以我只需要创建models.py文件和migrations文件夹即可)


最后,不要忘记在settings.py添加user_ex应用和更新数据库。


到了上代码的时候了,先看看models.py

#coding:utf-8

from django.db import models
from django.contrib.auth.models import User

class User_ex(models.Model):
    """User models ex"""
    user = models.ForeignKey(User)   #和User关联的外键
    valid_code = models.CharField(max_length = 24)   #验证码
    valid_time = models.DateTimeField(auto_now = True) #验证码有效时间

    def __unicode__(self):
        return u'%s' % (self.valid_code)


为了方便验证数据,我把重置密码的页面做成一个form表单,对应django也写了一个form表单模型:

#coding:utf-8
from django import forms
from django.core.exceptions import ValidationError
from django.contrib.auth.models import User
from user_ex.models import User_ex
from django.utils import timezone

class ForgetPwdForm(forms.Form):
    """Forget the password and try to find it"""
    email = forms.EmailField(label=u'用户邮箱', 
        widget=forms.EmailInput(attrs={'class':'form-control', 'id':'email','placeholder':u'请输入您注册时用的邮箱'}))
    pwd_1 = forms.CharField(label=u'新的密码', max_length=36,
        widget=forms.PasswordInput(attrs={'class':'form-control', 'id':'pwd_1','placeholder':u'请输入6-36位的密码'}))
    pwd_2 = forms.CharField(label=u'再输一遍', max_length=36,
        widget=forms.PasswordInput(attrs={'class':'form-control', 'id':'pwd_2','placeholder':u'重复新的密码确保正确'}))
    check_code = forms.CharField(label=u' 验证码', 
        widget=forms.TextInput(attrs={'class':'form-control', 'id':'check_code','placeholder':u'输入验证码'}))

    #验证邮箱是否存在
    def clean_email(self):
        email = self.cleaned_data.get('email')
        users = User.objects.filter(email = email)

        if users.count() == 0:
            raise ValidationError(u'该邮箱没有被注册,请检查')
        return email

    #验证两个新密码是否一致
    def clean_pwd_2(self):
        pwd_1 = self.cleaned_data.get('pwd_1')
        pwd_2 = self.cleaned_data.get('pwd_2')

        if pwd_1 != pwd_2:
            raise ValidationError(u'两次输入的密码不一致,再输入一次吧')
        return pwd_2

    #验证验证码是否正确
    def clean_check_code(self):
        try:
            #获取对应的用户
            email = self.cleaned_data.get('email')
            check_code = self.cleaned_data.get('check_code')
            user = User.objects.get(email = email)

            #获取对应的用户拓展信息,验证验证码
            user_ex = User_ex.objects.filter(user = user)
            if user_ex.count > 0:
                user_ex = user_ex[0]
            else:
                raise ValidationError(u'未获取验证码')
            
            if user_ex.valid_code != check_code:
                raise ValidationError(u'验证码不正确')
            
            #验证有效期
            now = timezone.now() #用这个回去当前时间
            create = user_ex.valid_time

            #两个datetime相减,得到datetime.timedelta类型
            td = now - create
            if td.seconds/60 >= 10:
                raise ValidationError(u'验证码失效')

            return check_code
        except Exception as e:
            raise ValidationError(u'验证码不正确或失效')

该表单可以验证邮箱、验证码和验证码有效期。有效期是为了安全期间而设定的。


模型和表单模型都有了,进一步写一下处理方法。这里需要两个处理方法:一是表单的处理方法,一个是获取验证码的处理方法。

#coding:utf-8
from django.http import HttpResponse
from django.shortcuts import render_to_response, render
from django.contrib.auth import logout,authenticate,login
from django.contrib.auth.models import User
from django.core.mail import EmailMultiAlternatives
import json, time, datetime, random, string

from .forms import ForgetPwdForm
from user_ex.models import User_ex
from django.utils import timezone

def password_lost(request):
    """forgot password deals"""
    data = {}
    data['form_title'] = u'重置密码'
    data['submit_name'] = u' 确定 '

    if request.method == 'POST':
        #表单提交
        form = ForgetPwdForm(request.POST)
        
        #验证是否合法
        if form.is_valid():
            #修改数据库
            email = form.cleaned_data['email']
            pwd = form.cleaned_data['pwd_2']
            user = User.objects.get(email = email)
            user.set_password(pwd)
            user.save()

            #删除验证码
            ex = User_ex.objects.filter(user=user)
            if ex.count() > 0:
                ex.delete()

            #重新登录
            user = authenticate(username=user.username, password=pwd)
            if user is not None:
                login(request, user)

            #页面提示
            data['goto_url'] = reverse('user_info')
            data['goto_time'] = 3000
            data['goto_page'] = True
            data['message'] = u'重置密码成功,请牢记新密码'
            return render_to_response('message.html',data)
    else:
        #正常加载
        form = ForgetPwdForm()
    data['form'] = form
    return render(request, 'user/pwd_forget.html', data)

def get_email_code(request):
    """get email code"""
    email = request.GET.get('email', '')
    code = ''.join(random.sample(string.digits + string.letters, 6))

    data = {}
    data['success'] = False
    data['message'] = ''
    
    try:
        #检查邮箱
        users = User.objects.filter(email = email)
        if len(users)==0:
            data['success'] = False
            data['message'] = u'此邮箱未注册'
            raise Exception, data['message']
        
        user = users[0]

        #检查短时间内是否有生成过验证码
        user_ex = User_ex.objects.filter(user = user)
        if len(user_ex)>0:
            user_ex = user_ex[0]

            #两个datetime相减,得到datetime.timedelta类型
            create_time = user_ex.valid_time
            td = timezone.now() - create_time
            if td.seconds < 60:
                data['message'] = u'1分钟内发送过一次验证码'
                raise Exception, data['message']
        else:
            #没有则新建
            user_ex = User_ex(user = user)

        #写入数据库
        user_ex.valid_code = code
        user_ex.valid_time = timezone.now()
        user_ex.save()

        #发送邮件
        subject=u'[yshblog.com]激活您的帐号'
        message=u"""
            <h2>杨仕航的博客(<a href='http://yshblog.com/' target=_blank>yshblog.com</a>)<h2><br />
            <p>重置密码的验证码(有效期10分钟):%s</p>
            <p><br/>(请保管好您的验证码)</p>
            """ % code

        send_to=[email]
        fail_silently=True  #发送异常不报错

        msg=EmailMultiAlternatives(subject=subject,body=message,to=send_to)
        msg.attach_alternative(message, "text/html")
        msg.send(fail_silently)

        data['success'] = True
        data['message'] = 'OK'
    except Exception as e:
        pass
    finally:
        return HttpResponse(json.dumps(data), content_type="application/json")

其中message.html模版是显示消息的,可以参考我的网站搭建(第13天) 用户认证:前言博文的末尾。

这里的获取验证码处理方法返回的是json数据。因为前端页面打算放置一个按钮,用ajax方式提交请求。

前端页面稍微复杂一点,需要计时。获取验证码之后,倒计时1分钟。避免获取频率过高。


我写的这个前端页面是继承了一个基础页面。基础页面已经引用了jQuery库。

{% extends "base.html" %}

{% block title %}杨仕航的博客{% endblock %}

{% block content %}
    {#通用表单页面#}
    <div class="row">
        <div class="col-md-6 col-md-offset-3" >
            <div id="panel_form" class="panel panel-default">
            <div class="panel-body">
            <h3 class="form_title">{{form_title}}</h3>
            <form class="main_form" method='post'>
                {%csrf_token%}
                {% for field in form %}
                    {# 区分是否是hidden字段 #}
                    {% if field.is_hidden %}
                        {{ field }}
                    {% else %}
                        <div class="input-group">
                            <label class="input-group-addon" for="{{ field.id_for_label }}">
                                {{ field.label }}
                            </label>
                            {{ field }}
                        </div>
                        {# 错误提示信息 #}
                        <p class="text-danger text-right">
                            {{ field.errors.as_text }}
                        </p>
                    {%endif%}
                {%endfor%}

                <div class="text-right form_btn">
                    <a class="btn btn-primary" id="btn_code">获取验证码</a>
                    <input class="btn btn-primary" id="btn_submit" type="submit" value="{{submit_name}}"/>
                </div>
            </form>
            </div>
            </div>
        </div>
    </div>
{% endblock %}

{% block extra_footer %}
    {#获取验证码#}
    <script type="text/javascript">
        function time_run(time_check){
            time_check -= 1;
            $("#btn_code").text("("+time_check+")重新获取");
            if(time_check>0){
                //setTimeout('time_run('+time_check+')', 1000);
                setTimeout(function(){time_run(time_check)}, 1000);
            }else{
                $("#btn_code").text("获取验证码");
                $("#btn_code").removeClass("disabled");
            }
        }

        $(document).ready(function() {
            $("#btn_code").click(function(){
                //获取填写的邮箱,并检查
                var email_tip = $(".text-danger")[0];
                var email = document.getElementById("email").value;
                email_tip.innerText = '';

                var myreg = /^([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|\_|\.]?)*[a-zA-Z0-9]+\.[a-zA-Z]{2,3}$/;
                if(!myreg.test(email)){
                    email_tip.innerText = '* 请正确地填写邮箱'
                    return false;
                }

                //发送验证码
                $.ajax({
                    type:"GET",
                    url:"{% url 'get_email_code' %}?email=" + email,
                    cache:false,
                    dataType:'json',
                    success:function(result){
                        if(result['success']){
                            //提示
                            var code_tip = $(".text-danger")[3];
                            code_tip.innerText = '已经发送验证码到您填写的邮箱中'

                            //禁用按钮
                            $("#btn_code").addClass("disabled");
                            //倒计时
                            time_run(60);
                        }else{
                            alert(result['message']);
                        }                        
                    },
                    error:function(XMLHttpRequest, textStatus, errorThrown){
                        alert("获取出错,稍后再试一下吧。")
                    }
                });
            });
        });
    </script>

    <style type="text/css">
        .form_title{
            margin-bottom: 1em;
            padding-bottom: 0.5em;
            border-bottom: 1px #ccc solid;
        }
        .main_form div, .form_btn .btn{
            margin-top:1em;
        }
        #panel_form{
            margin-top: 2em;
            margin-bottom: 3em;
        }
    </style>
{% endblock %}

倒计时部分,你可以看成是一个递归。每次调用都把变量减去1,直到控制变量为0时才跳出递归。

最后,再加上url路由设置,和登录页面加上对应的链接即可。

url(r'password_lost', 'user_ex.views.password_lost', name='password_lost'),
url(r'get_email_code', 'user_ex.views.get_email_code', name='get_email_code'),


最终效果如下:

   

上一篇:树莓派用开关三极管控制散热风扇

下一篇:我对Python装饰器的理解

评论列表

智慧如你,不想发表一下意见吗?

新的评论

清空