背景
什么?做找回密码功能对于一个 SaaS 平台来说还需要考虑背景问题吗?
最近在看一本书叫 —— Work Rules 谷歌工作法 ,其中介绍到了一些企业软件设计,提到了很有趣的一点。为什么许多现在的 SaaS 平台都不在用户注册页面强制用户输入两次密码来使其记牢 (听起来没啥用)?调查发现,这些没有双重验证的产品的用户粘性大多比其他同类应用高,因为用户有更大的几率去使用「找回密码」功能。潜意识中就使其更容易记住也愿意去花时间使用这个产品,因为「不然不值我花了找回密码的宝贵 30 秒」... (主要是让写此功能的程序员能得到一点慰藉)。
代码
我已经在 Eugrade (原名 Pokers)实现了这个功能,使用了阿里云邮件推送产品,每天 2000 封良心:)
之后可能还会完善一下各种验证、邮件推送、消息推送等,于是干脆建立了一个新表 —— Temp,五个字段:
/*
* @var int id 自增序号
* @var string k 唯一标记
* @var string v 内容值
* @var integer d 过期时间
* @var string e 冗余
*/
找回密码的逻辑大概是:
- 输入邮箱,带随机参数请求后端
- 后端验证邮箱用户存在,根据随机参数生成唯一的 Token
- 保存数据至数据库,k 为上述 Token,v 为随机验证码,d 为一天之后的 unix 时间戳
- 调用阿里云邮件推送发送带验证码的邮件,这里直接使用 PHP SDK (https://help.aliyun.com/document_detail/29460.html)
- 后端返回 Token,前端保存
- 用户前往邮箱接收,前端输入验证码请求后端
- 后端根据 Token 与验证码值判断正确性
- 重置密码为 12345678
if (!!$array && !!$array_user) { //存在验证与用户
if ((int) $time <= (int) $array[0]['d']) { //未过期
if ((int) $array[0]['v'] == (int) $input) { //判断正确
//重设密码
$t = Lazer::table('users')->limit(1)->where('email', '=', (string) $email)->find();
$t->set(array(
'pwd' => md5(md5('12345678') . md5('12345678')),
));
$t->save();
//删除验证记录
Lazer::table('temp')->limit(1)->where('k', '=', (string) $token)->delete();
$status = 1;
$code = 107;
$mes = 'Successfully reset your password to 12345678';
} else {
$status = 0;
$code = 111;
$mes = 'Incorrect verification code';
}
} else {
$status = 0;
$code = 106;
$mes = 'This request has been depreciated';
}
} else {
$status = 0;
$code = 108;
$mes = 'Illegal request';
}
↑ 验证的核心部分 PHP 代码
include_once 'database/aliyun-php-sdk-core/Config.php';
use Dm\Request\V20151123 as Dm;
$iClientProfile = DefaultProfile::getProfile("cn-hangzhou", "xxx", "xxx");
$client = new DefaultAcsClient($iClientProfile);
$request = new Dm\SingleSendMailRequest();
$request->setAccountName("noreply@eugrade.com");
$request->setFromAlias("Eugrade");
$request->setAddressType(1);
$request->setReplyToAddress("true");
//生成保存参数
$token = md5(md5($ran) . md5($name));
$expire_date = strtotime('+1day', time());
$rand = rand(0, 32768);
//判断验证目的
switch ($name) {
case 'reset_pwd': //重设密码
if (!empty($_POST['email']) && filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
$email_get = input($_POST['email']);
$array = Lazer::table('users')->limit(1)->where('email', '=', $email_get)->find()->asArray();
if (!!$array) {
//建立验证记录
$this_id = Lazer::table('temp')->lastId() + 1;
$row = Lazer::table('temp');
$row->id = $this_id;
$row->k = (string) $token; //标记
$row->v = (string) $rand; //值
$row->d = (int) $expire_date; //过期时间
$row->save();
//发送邮件
$request->setTagName("resetpwd");
$request->setToAddress($email_get);
$request->setSubject("Eugrade Password Reset");
$request->setHtmlBody("Hi there,<br/><br/>This is your verification code for Eugrade password reset request: <b style='font-size:20px'>" . (string) $rand . "</b><br/>Please verify your request within one day, thank you.<br/><br/><a href='https://www.eugrade.com'>www.eugrade.com</a>");
$client = $client->getAcsResponse($request);
$status = 1;
$code = 105;
$mes = 'Successfully sent a request';
} else {
$status = 0;
$code = 107;
$mes = 'No such user with this email address';
}
} else {
$status = 0;
$code = 106;
$mes = 'Illegal Request';
}
break;
default:
$status = 0;
$code = 104;
$mes = 'Illegal request';
break;
}
↑ 创建验证记录/邮件发送核心 PHP 代码
send_email() {
this.send.loading = true;
var query_string = "email="+ this.input.email +"&name=reset_pwd&ran=" + Math.ceil(Math.random() * 100000);
axios.post(
'interact/create_ver.php',
query_string
)
.then(res => {
if (res.data.status) {
this.send.status = true;
this.$message.success(res.data.mes);
this.send.token = res.data.token;
var interval = setInterval(function () {
if(antd.send.count > 0){
antd.send.count--;
antd.send.text = 'Resend Code('+antd.send.count+')';
}else{
antd.send.count = 60;
antd.send.loading = false;
antd.send.text = 'Resend Code';
clearInterval(interval);
}
}, 900);
} else {
this.send.loading = false;
this.$message.error(res.data.mes);
}
})
},
check_code() {
this.send.loading = true;
var query_string = "email="+ this.input.email +"&name=reset_pwd&input=" + this.input.code + "&token=" + this.send.token;
axios.post(
'interact/check_ver.php',
query_string
)
.then(res => {
if (res.data.status) {
this.$message.success(res.data.mes);
this.send.status = false;
setTimeout('window.location.href = "login.html"',1000);
} else {
if(this.send.count == 60){
this.send.loading = false;
}
this.$message.error(res.data.mes);
}
})
}
↑ 前端核心 Vue.js 代码
<a-form class="login-form"
style="padding: 0px 20px">
<a-form-item has-feedback style="margin-bottom: 15px;">
<a-input size="large" v-model="input.email" placeholder="Email">
<a-icon slot="prefix" type="mail" style="color: rgba(0,0,0,.25)" />
</a-input>
</a-form-item>
<a-form-item v-if="send.status">
<a-input size="large" placeholder="Verification Code" v-model="input.code">
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)" />
</a-input>
</a-form-item>
<a-form-item v-if="!send.status">
<a-button @click="send_email" style="width:100%" :disabled="send.loading" type="primary" size="large" html-type="submit" class="login-form-button">
Send Code
</a-button>
</a-form-item>
<a-form-item v-if="send.status">
<div style="display: flex">
<a-button @click="check_code" style="width:49%;margin-right: 1%" type="primary" size="large" html-type="submit" class="login-form-button">
Check Code
</a-button>
<a-button @click="send_email" v-html="send.text" :disabled="send.loading" style="width:49%" type="default" size="large" html-type="submit" class="login-form-button">
</a-button>
</div>
</a-form-item>
<p style="text-align: center;margin: 0px;margin-bottom: -15px;margin-top: 10px;"><a
href="login.html">Return to Log in</a></p>
</a-form>
↑ 前端核心 HTML 代码
后记
接下来真的要鸽好久的... 🙂