同福

做个用户管理系统(35)——通过绑定邮箱重置密码【20201224】

介绍

介绍

福哥今天带着大家完成了通过绑定邮箱重置密码的后一部分功能——重置密码功能。这一部分功能的思路是这样的,首先点开发到邮箱里的邮件正文里面的重置链接,这是一个重置密码的表单页面,用户可以直接设置一个新密码。

不过,这里面会校验传入的验证码是否有效,如果验证码有效的话才可以完成密码的重置操作。

并且,如果密码重置成功后,验证码会被记录到Redis数据库里面,当用户使用同一个表单页面试图再次重置密码的时候会被告知验证码无效,这样就避免了验证码重复使用的问题了。

模型user

resetPassByEmail

public function resetPassByEmail(string $dataEncrypted, string $newPwd):bool {
    $tfdo = $this->tfphp->getDatabase()->getTFDO();
    $tfredis = $this->tfphp->getDatabase()->getRedis();
    $myAES = new TFAES($this->tfphp);

    $dataMD5Key = md5($dataEncrypted). "resetPassByEmail";
    if($tfredis->get($dataMD5Key) == $dataEncrypted){

        return 1;
    }

    $timestamp = date("YmdH0000");
    $data = $myAES->decrypt($dataEncrypted, TFConfig::get("projectAESPK", "system"). $timestamp, "");
    $arr = unserialize($data);
    if(!is_array($arr)
        || $arr['action'] != "resetPassByBindEmail"
        || $arr['id'] == 0){
        $timestamp = date("YmdH0000", strtotime("-1 hour"));
        $data = $myAES->decrypt($dataEncrypted, TFConfig::get("projectAESPK", "system"). $timestamp, "");
        $arr = unserialize($data);
        if(!is_array($arr)
            || $arr['action'] != "resetPassByBindEmail"
            || $arr['id'] == 0){

            return 1;
        }
    }

    $userID = $arr['id'];

    $userInfo = $this->getByTable("user", array($userID));
    if($userInfo == null){

        return 2;
    }

    $ret = $tfdo->update("user", array(
        'passwd'=>md5($newPwd)
    ), null, "userID = @int", array(
        $userID
    ));
    if(!$ret){

        return 3;
    }

    $tfredis->set($dataMD5Key, $dataEncrypted);
    $tfredis->expire($dataMD5Key, 7200);

    return 0;
}

接口控制器

user_process

protected function user_process(){
    $req = $this->tfphp->getRequest();
    $post = $req->post;
    $user = new user($this->tfphp);
    $data = $req->get->sn;
    $npass = $post->get("npass");
    $cpass = $post->get("cpass");

    try{
        // request test
        if($npass == "" || $cpass == ""){

            return $this->tfphp->getResponse()->responseJSON_CM(200, 1001101, "错误请求");
        }
        if($npass != $cpass){

            return $this->tfphp->getResponse()->responseJSON_CM(200, 1001101, "两次输入的新密码不一样");
        }

        // forgot by email
        $ret = $user->resetPassByEmail($data, $npass);
        switch ($ret){
            case 1:
                return $this->tfphp->getResponse()->responseJSON_CM(200, 1001102, "验证码无效");
                break;
            case 2:
                return $this->tfphp->getResponse()->responseJSON_CM(200, 1001103, "用户不存在");
                break;
            case 3:
                return $this->tfphp->getResponse()->responseJSON_CM(200, 1001104, "重置密码失败");
                break;
        }
    }
    catch(\TypeError $e){

        return $this->tfphp->getResponse()->responseJSON_CM(200, 1001101, "错误请求");
    }

    // output
    return $this->tfphp->getResponse()->responseJSON_CM(200, 0, "OK");
}

视图模板

HTML代码

<!-- bind Email form begin -->
<div class="row login-form">
    <div class="col-sm-12">
        <h3 class="text-center">重置密码</h3>
        <p>请输入您的新密码</p>
        <form>
            <div class="form-group">
                <label>新密码</label>
                <input class="form-control" type="password" name="npass" />
            </div>
            <div class="form-group">
                <label>新确认密码</label>
                <input class="form-control" type="password" name="cpass" />
            </div>
            <div class="form-group">
                <button class="btn btn-primary btn-sm form-control">重置密码</button>
            </div>
        </form>
    </div>
</div>
<!-- bind Email form end -->

JS代码

$('form').form({
    url: "<% $TFReq->server->BASE_URI %>api/member/resetPassByEmail?sn=<% $TFReq->get->data %>",
    method: "post",
    validations: [
        {type:"empty", name:"npass", msg:"请填写新密码"},
        {type:"min", value:6, name:"npass", msg:"新密码最少6个字"},
        {type:"empty", name:"cpass", msg:"请填写新确认密码"}
    ],
    onSuccess: function (d) {
        if(d.errcode == 0){
            document.location = '<% $TFReq->server->BASE_URI %>resetPassByEmailOK.htm?sn=<% $TFReq->get->data %>';
        }
        else{
            $('form').tips({
                text:d.errmsg
            });
        }
    },
    onError: function (d) {
        $('form').tips({
            text:"服务器响应错误"
        });
    },
    onValidationError: function (form, name, msg) {
        $('form').tips({
            text:msg
        });
        $('form').find('[name="'+ name +'"]').focus();
    }
});

设计要点

验证码有效期限制

判断

$dataMD5Key = md5($dataEncrypted). "resetPassByEmail";
if($tfredis->get($dataMD5Key) == $dataEncrypted){

    return 1;
}

设置

$tfredis->set($dataMD5Key, $dataEncrypted);
$tfredis->expire($dataMD5Key, 7200);

讲解

模型user

resetPassByEmail

首先检验Redis里是否有包含验证码MD5哈希码的值,如果有则表示用户已经使用这个验证码重置过密码了,就要拒绝再次操作。

接着校验验证码是否有效,如果无效就要报错。

然后检验用户是否存在,不存在就报错。

再来就是修改登录密码为新密码了。

最后要将验证码的MD5哈希码的值记录到Redis里,设置有效期2个小时,因为两个小时后验证码也会过期,就不存在安全问题了。

接口控制器

user_process

这是一个标准表单的处理后台程序,使用resetPassByEmail重置用户的密码。

视图模板

HTML代码

这是重置密码表单界面,区别于修改密码表单,不需要提供原始密码。

JS代码

这是一个标准表单的JS驱动程序。

设计要点

验证码有效期限制

因为我们的验证码是通过AES算法生产的,虽然通过加入时间戳可以限制验证码的有效期为一个小时之内,但是不能保证在这一个小时之内用户不会反复地进行重置密码操作。

为了解决这个问题,我需要借住数据库,相比MySQL来说还是Redis更加适合做这个。

思路是这样的,我们将用过的验证码存到Redis里面,在校验验证码的时候判断Redis里面是否存有验证码,如果存有了就表示这个验证码已经用过了,就不能再用了。因为Redis存的验证码是由有2个小时的有效期的,而验证码的时间戳是1个小时,虽然在验证的时候会有前一个小时的补差,但是2个小时肯定是够用的了。

效果

重置密码表单界面。

615a7e2542b7579f.jpg

重置完密码界面。

因为resetPassByEmailOK页面就是一个静态页面,福哥不在这里提供说明了,大家可以自己建立起来。

5558b82f4f16267f.jpg

总结

福哥今天带着童鞋们完成了提供绑定邮箱重置密码的后一部分功能,至此通过绑定邮箱重置密码的功能就全部完成了。

下一课,福哥将带着童鞋们实现通过安全问题重置密码的功能了!