前言
前面我在 typecho如何实现前台登录/注册 一文中详细介绍了一下 前台登录/注册 的实现原理与细节,当时是以登录为例说明的,因为登录比较简单,代码量也比较少,但考虑再三之后,还是决定单独写一篇关于注册的文章,毕竟注册还是要复杂一些,而且还涉及到邮箱验证。不过,这篇文章就不再解释原理了,而是直接贴出核心代码,然后对部分要点做一些简单的解释,方便后续有需要的人可以直接复用。
1. 表单界面
下图就是前端注册表单的效果,也是基于 Bootstrap 5
实现的模态窗。
核心代码如下:
<form action="<?php $this->options->registerAction(); ?>" method="post" name="register" role="form">
<div class="modal-header border-0">
<h1 class="modal-title fs-5" id="registerModalLabel"><?php _e('注册') ?></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body pb-0">
<div class="mb-3">
<input type="text" name="name" class="form-control" placeholder="<?php _e('用户名'); ?>" autofocus />
</div>
<div class="mb-3">
<input type="email" name="mail" class="form-control" placeholder="<?php _e('邮箱'); ?>" />
</div>
<div class="d-flex mb-3 gap-2 align-items-center">
<input type="text" name="code" class="form-control" placeholder="<?php _e('验证码'); ?>" required />
<button type="button" class="btn btn-outline-danger text-nowrap send-verify-code"><?php _e('发送验证码到邮箱'); ?></button>
</div>
<div class="mb-3">
<input type="password" name="password" class="form-control" placeholder="<?php _e('新密码'); ?>" required />
</div>
<div>
<input type="password" name="confirm" class="form-control" placeholder="<?php _e('确认新密码'); ?>" required />
</div>
</div>
<div class="modal-footer border-0 justify-content-between">
<button type="submit" class="btn btn-dark"><?php _e('注册'); ?></button>
<div class="d-flex align-items-center">
<span class="me-1"><?php _e('已有账号,去') ?></span>
<a href="#" class="link-secondary link-offset-2 link-underline-opacity-0 link-underline-opacity-100-hover" data-bs-toggle="modal" data-bs-target="#loginModal"><?php _e('登录'); ?></a>
</div>
</div>
</form>
这是一个很常规的注册表单,唯一不同的是,这里有一个“发送验证码到邮箱”的功能,是用于验证邮箱真实性的,具体实现可以参考 typecho注册实现邮箱验证,这里就假设已经获取到验证码了。
2. 注册逻辑
2.1 提交请求
和登录一样,注册请求也是前端通过JS
提交,核心代码如下:
const registerForm = document.querySelector("#registerModal form");
formSubmit(registerForm, "frontRegister");
function formSubmit(form, doMethod) {
if (!form) {
return;
}
form.addEventListener("submit", (e) => {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
data[key] = value;
}
data.do = doMethod;
axios
.post(form.action, data, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
})
.then(function (response) {
const data = response.data;
showToast(data.message, data.success ? "success" : "error");
if (data.success) {
window.location.reload();
}
})
.catch(function (error) {
console.log(error);
});
});
}
这段代码和登录时的几乎一模一样,唯一不同的是formSubmit()
函数的两个参数是注册相关的了。
2.2 后端处理
直接上代码吧!
class Register extends Users implements ActionInterface
{
public function action()
{
$this->on($this->request->is('do=frontRegister'))->frontRegister();
// 不允许后端注册
$this->response->redirect($this->options->index);
// protect
// $this->security->protect();
// ... ...
}
function frontRegister()
{
/** 如果已经登录 */
if ($this->user->hasLogin()) {
echo json_encode([
'success' => true,
'message' => _t('您已经登录了')
]);
exit;
}
if (!$this->options->allowRegister) {
echo json_encode([
'success' => false,
'message' => _t('注册功能已关闭')
]);
exit;
}
$error = $this->formVerify();
if (!empty($error)) {
echo json_encode([
'success' => false,
'message' => array_values($error)[0]
]);
exit;
}
$name = $this->request->get('name');
$mail = $this->request->get('mail');
$password = $this->request->get('password');
$hasher = new PasswordHash(8, true);
$dataStruct = [
'name' => $name,
'mail' => $mail,
'screenName' => $name,
'password' => $hasher->hashPassword($password),
'created' => $this->options->time,
'group' => 'subscriber'
];
$insertId = $this->insert($dataStruct);
$this->db->fetchRow($this->select()->where('uid = ?', $insertId)
->limit(1), [$this, 'push']);
$this->user->login($name, $password);
Cookie::delete('__typecho_first_run');
$cacheKey = $this->getVerifyCodeCacheKey($mail);
FileCache::delete($cacheKey);
echo json_encode([
'success' => true,
'message' => _t('注册成功')
]);
exit;
}
function verifyCode($code): bool
{
$mail = $this->request->get('mail');
$cacheKey = $this->getVerifyCodeCacheKey($mail);
$verifyCode = FileCache::get($cacheKey);
if ($verifyCode && $verifyCode === $code) {
return true;
} else {
return false;
}
}
function formVerify(bool $isForget = false)
{
$validator = new Validate();
if (!$isForget) {
$validator->addRule('name', 'required', _t('必须填写用户名称'));
$validator->addRule('name', 'minLength', _t('用户名至少包含2个字符'), 2);
$validator->addRule('name', 'maxLength', _t('用户名最多包含32个字符'), 32);
$validator->addRule('name', 'xssCheck', _t('请不要在用户名中使用特殊字符'));
$validator->addRule('name', [$this, 'nameExists'], _t('用户名已经存在'));
$validator->addRule('mail', [$this, 'mailExists'], _t('电子邮箱地址已经存在'));
}
$validator->addRule('mail', 'required', _t('必须填写电子邮箱'));
$validator->addRule('mail', 'email', _t('电子邮箱格式错误'));
$validator->addRule('mail', 'maxLength', _t('电子邮箱最多包含64个字符'), 64);
$validator->addRule('code', 'required', _t('请输入验证码'));
$validator->addRule('code', [$this, 'verifyCode'], _t('验证码错误'));
$validator->addRule('password', 'required', _t('必须填写密码'));
$validator->addRule('password', 'minLength', _t('为了保证账户安全, 请输入至少六位的密码'), 6);
$validator->addRule('password', 'maxLength', _t('为了便于记忆, 密码长度请不要超过十八位'), 18);
$validator->addRule('confirm', 'confirm', _t('两次输入的密码不一致'), 'password');
return $validator->run($this->request->from('name', 'mail', 'code', 'password', 'confirm'));
}
function getVerifyCodeCacheKey($mail)
{
return 'verify_code_' . $mail;
}
}
这里需要做一些简单的说明:
- 参数命中
do=frontRegister
后,请求交由frontRegister()
方法处理,而其它方式的注册请求(其实就是后台注册),我这里就直接屏蔽了,所有的请求全部跳转到首页。当然,也可以保留,但要实现和前台注册相同的逻辑,否则,就会留下一个不小的漏洞; - 表单验证还是沿用了用户名和邮箱不能重复的逻辑,但邮箱多了一个校验验证码的逻辑,具体是在
verifyCode($code)
方法中实现的; - 在校验验证码时用到的
FileCache::get($cacheKey)
和注册成功后用到的FileCache::delete($cacheKey)
是基于文件实现的缓存机制,这个也在typecho注册实现邮箱验证一文中进行了详细的介绍。
结语
好了,这就是注册的完整核心代码了,看上去好像也并不复杂,毕竟和登录的实现思路是一致的。
评论2
Huo
这太强了,一点看不懂,头大,就感觉很牛逼。
老朱
这个博客网站基本用不上