前言
前面的文章说过,我在开发 一起学笛子 网站的时候,用的是邮箱验证的方式,这篇文章将详细介绍一下具体的实现过程。
实际上,在 一起学笛子 这个网站中,有两处用到了邮箱验证,一个是注册,另一个是重置密码(忘记密码),而且这两个地方的用法是一模一样的,所以,我接下来还是以熟悉的注册场景举例说明。
1. 前端请求
前端界面和注册是同一个表单,如下图所示:
核心代码在 typecho前台注册核心代码 中已经介绍过了,所以这里就不展示了。这次我们重点关注“发送验证码到邮箱”这个按钮的点击事件,下面是其对应的JS
代码:
document.querySelectorAll(".send-verify-code").forEach((item) => {
item.addEventListener("click", (e) => {
const btn = e.target;
if (!btn.form) {
return;
}
const form = btn.form;
axios
.post(
form.action,
{
do: "sendVerifyCode",
type: form.getAttribute("name"),
mail: form.mail.value,
},
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
)
.then(function (response) {
const data = response.data;
let type;
if (data.success) {
type = "success";
const expires = new Date().getTime() + 60 * 1000;
localStorage.setItem("countdown", expires);
countdown(btn);
} else {
type = "error";
}
showToast(data.message, type);
})
.catch(function (error) {
console.log(error);
});
});
});
- 前文说过,网站有两个地方用到了“发送验证码到邮箱”的功能,因此,这里通过
document.querySelectorAll(".send-verify-code")
对每一个按钮都注册一个点击事件; - 请求直接提交到了
form.action
,和注册是同一个处理接口,因此,发送验证码的请求也需要到Register
类中处理。当然,我这是为了简单才这么做的,你也可以新建一个专门处理发送验证码请求的类,然后,做好路由映射即可,具体可以参考typecho如何实现前台登录/注册这篇文章; do
参数设置成了sendVerifyCode
,由此可以推测,后端PHP
代码中,应该有一个sendVerifyCode()
方法与之对应。
2. 后端处理
后端处理稍微复杂一些,主要分为以下几个步骤。
2.1 邮箱验证
并不是所有的有效邮箱都应该发送验证码的,还跟具体的应用场景有关,如果是注册,则没有注册过的邮箱才应该发送验证码,而如果是重置密码,则应该刚好相反,核心代码如下:
$mail = $this->request->get('mail');
$type = $this->request->get('type');
$notExists = $this->mailExists($mail);
if ($type == 'register') {
if (!$notExists) {
echo json_encode([
'success' => false,
'message' => _t('该邮箱已被注册了')
]);
exit;
}
} elseif ($type == 'forgot') {
if ($notExists) {
echo json_encode([
'success' => false,
'message' => _t('该邮箱还没有注册')
]);
exit;
}
} else {
echo json_encode([
'success' => false,
'message' => _t('参数错误')
]);
exit;
}
这里的register
和forgot
是表单名称,用于区分请求是来自于注册,还是重置密码。
2.2 生成验证码
通常情况下,验证码应该是随机生成的一串四位或六位数字字符串,我这里用的是六位,代码如下:
function randCode($length): string
{
$result = '';
for ($i = 0; $i < $length; $i++) {
$result .= rand(0, 9);
}
return $result;
}
2.3 发送验证码
由于我们是通过邮箱发送验证码,因此,这里还需要提前有两点准备。
第一,开启发送者邮箱的POP3/SMTP/IMAP
服务,这个不同的邮件服务商开启的位置不同,但都非常简单,就不介绍了,自行搜索即可。
第二,引入一个发送邮件的类库,如PHPMailer
,不过,如果你跟我一样,网站中恰好集成了评论发送邮件通知的插件,那么也可以直接使用插件的发送邮件功能,它们通常已经集成了第三方的相关类库,自己再添加就重复了。
我这里用的是泽泽大佬开发的CommentNotifier,它内部集成的就是PHPMailer
。
具体实现代码如下:
$verifyCode = $this->randCode(6);
$plugin = $this->options->plugin('CommentNotifier');
if ($plugin) {
$param = [
'to' => $mail,
'fromName' => $plugin->fromName,
'subject' => '【' . $plugin->fromName . '】' . _t('验证码'),
'html' => _t('您的验证码是:') . $verifyCode . _t(',仅用于邮箱验证,请勿泄露给他人。')
];
\TypechoPlugin\CommentNotifier\Plugin::resendMail($param);
$cacheKey = $this->getVerifyCodeCacheKey($mail);
//缓存30分钟
FileCache::set($cacheKey, $verifyCode, 30 * 60);
echo json_encode([
'success' => true,
'message' => _t('验证码已发送,请查收')
]);
} else {
echo json_encode([
'success' => false,
'message' => _t('验证码发送失败,请联系管理员')
]);
}
可以看到,基于插件实现还有一个好处就是,部分公共参数(如发送者邮箱,账号密码之类)已经通过插件配置好了,这里直接发送内容即可,非常简单。
在上述段代码中,FileCache::set($cacheKey, $verifyCode, 30 * 60)
是用于缓存验证码的,以供注册和重置密码时,验证邮箱使用。
2.4 缓存验证码
缓存验证码有很多实现方式,最常见的是基于SESSION
实现,但typecho的设计是登录之后才启用SESSION
,不到万不得已,我不想打破这个设计。
其他方案要么需要启用扩展,如OPcache
,要么需要引入中间件,如Memcached
、Redis
等,都有高射炮打蚊子的感觉,所以,我最终选择了比较简单的基于文件操作的方案,毕竟,发送邮箱验证码也不是一个高频的操作。
因此,我直接在var\Widget
目录下新建了一个FileCache.php
文件,具体代码如下:
<?php
namespace Widget;
class FileCache
{
private static $cacheDir = __TYPECHO_ROOT_DIR__ . '/cache';
public static function set($key, $value, $ttl = 0)
{
if (!is_dir(self::$cacheDir)) {
mkdir(self::$cacheDir, 0777, true);
}
$filePath = self::getCacheFilePath($key);
$data = serialize([$value, time() + $ttl]);
file_put_contents($filePath, $data);
self::clearExpired();
}
public static function get($key)
{
$filePath = self::getCacheFilePath($key);
if (!file_exists($filePath)) {
return null;
}
$data = unserialize(file_get_contents($filePath));
if (!is_array($data) || count($data) !== 2) {
return null; // 数据格式不正确,可能是文件损坏
}
list($value, $expire) = $data;
if ($expire > 0 && $expire < time()) {
// 缓存已过期,删除文件
unlink($filePath);
return null;
}
return $value;
}
public static function delete($key)
{
$filePath = self::getCacheFilePath($key);
if (file_exists($filePath)) {
unlink($filePath);
}
}
private static function clearExpired()
{
foreach (glob(self::$cacheDir . '*.cache') as $filePath) {
$data = unserialize(file_get_contents($filePath));
if (!is_array($data) || count($data) !== 2) {
unlink($filePath);
continue;
}
list($value, $expire) = $data;
if ($expire > 0 && $expire < time()) {
// 缓存已过期,删除文件
unlink($filePath);
}
}
}
private static function getCacheFilePath($key)
{
return self::$cacheDir . md5($key) . '.cache';
}
}
这里有两点需要说明一下的是:
- 代码需要在网站根目录下创建一个
cache
目录,因此,需要对根目录授予写入和执行权限; - 为了避免垃圾文件不断增加,这里在每次设置缓存的时候都会清理一次缓存,这种方式适合并发量不高的场景,如果并发量较高,则应该定时清理。
结语
好了,这就是实现邮箱验证的完整思路了,基于邮箱的注册方式虽然在国内比较少用,但在出海项目中可能会有用武之地。
一方面,邮箱在全球范围内广泛使用,无论国内还是国外用户,通常都拥有至少一个邮箱地址,而且不像手机号可能受到不同国家和地区运营商的限制以及国际短信费用等问题的影响。
另一方面,邮件系统相对稳定,注册邮件可以作为用户注册的记录留存,同时,也可以接收重要通知和信息,便于和用户进行沟通。
因此,了解一下这种方式,应该还是有一点意义的。
评论4
明月登楼
Typecho 建议开启首次评论必须人工审核,否则垃圾评论会非常的多!这样一次标记垃圾后,后面就会少很多,毕竟这些垃圾评论都是机器发的!
老朱
我现在是全人工审核,首次还不行,有些机器发的垃圾评论我人工审核都放行了,后来发现不对才删的🤣
明月登楼
呵呵,你要是境外服务器的话,我建议你弄个CloudFlare的验证,我给我的跨境电商客户就弄了,效果非常好,垃圾评论直接直线下降直至消失!
老朱
可以的,到时搞个试试😘