最近支持了云开发的自定义短信验证码登录功能。第一次体会到利用云开发自身能力,开箱即用的快感。所有的精力集中在业务逻辑和数据库设计上,不用花费过多的精力浪费在运维上。
环境准备
- 前往腾讯云控制台,开通云开发
- 打开云开发设置-匿名登录
- 前往腾讯云控制台,开通 SMS
- 打开 SMS,创建并审核通过短信模版
架构设计
云数据库
前往 CloudBase 控制台,创建 tcb-sms-auth 集合。集合字段信息如下:
代码语言:javascript复制expiration: 验证码过期时间
phone: 手机号
smsCode: 验证码
除了 expiration 字段,还需要一个多余的字段来防止验证码对同一手机,在规定时间内,重复发送。但是腾讯云 SMS 自带频控管理,所以不在数据库中添加这个字段。
云函数
支持 3 种 Action:
send(phone)
: 向手机号 phone 发送随机验证码verify(phone, smsCode)
: 检验手机验证码是否正确clear()
: 定时任务清空手机验证码(前往 cloudbase 控制台-云函数-设置定时 corn)
整体架构设计如下:所有的服务都封装在 services 目录下;index.js 是入口文件,解析 C 端传入的参数,从而调用对应的 service。
发送随机验证码
流程如下:
step1: 查询云数据库,清空 phone 之前的验证码。保证在同一时刻,对同一个 phone,只有一个 smsCode 有效
step2: 生成随机 6 位验证码,并将其存入云数据库
代码语言:javascript复制/**
* 生成验证码并存储到云数据库,发送短信
*
* @param {string} phone
* @param {object} ctx
*/
async function sendSmsCode(phone, ctx) {
const { db } = ctx;
// 1. 移除之前的验证码
const res = await db
.collection(config.collection)
.where({ phone })
.get();
if (res.data.length) {
await db
.collection(config.collection)
.where({ phone })
.remove();
}
// 2. 生成验证码
const smsCode = randomStr(6);
const period = 3 * 60 * 1000;
const doc = {
phone, // 电话号码
smsCode, // 短信验证码
expiration: Date.now() period // 过期时间
};
await db.collection(config.collection).add(doc);
// 3. 发送短信
await sendSms({
phone,
smsCode
});
}
step3: 调用腾讯云 SMS 服务,向 phone 发送 smsCode
代码语言:javascript复制/**
* 发送短信
*
* @param {object} params
*/
function sendSms(params) {
const { phone, smsCode } = params;
const {
TENCENTCLOUD_SECRETID,
TENCENTCLOUD_SECRETKEY,
TENCENTCLOUD_SESSIONTOKEN
} = process.env;
// 具体拼接请见:https://github.com/TencentCloud/tencentcloud-sdk-nodejs/blob/master/examples/sms/v20190711/SendSms.js
const cred = new Credential(
TENCENTCLOUD_SECRETID,
TENCENTCLOUD_SECRETKEY,
TENCENTCLOUD_SESSIONTOKEN
);
const client = new smsClient(cred, "ap-guangzhou");
const req = new models.SendSmsRequest();
req.SmsSdkAppid = config.SmsSdkAppid;
req.Sign = config.Sign;
req.ExtendCode = "";
req.SenderId = "";
req.SessionContext = "";
req.PhoneNumberSet = [` 86${phone}`];
req.TemplateID = config.TemplateID; // 模版类似:验证码是{1},有效期{2}
req.TemplateParamSet = [smsCode, 3]; // 3代表3分钟
return new Promise((resolve, reject) => {
client.SendSms(req, (err, res) => {
// 用于日志
console.log(">>> sms err is", err);
console.log(">>> sms res is", res);
if (err) {
err.code = "SMS_REQUEST_FAIL";
return reject(err);
}
const json = JSON.parse(res.to_json_string());
if (json.SendStatusSet[0].Code.toLowerCase() === "ok") {
return resolve();
}
const error = new Error(json.SendStatusSet[0].Message);
error.code = json.SendStatusSet[0].Code;
return reject(error);
});
});
}
检验验证码有效性
利用聚合搜索,查询符合以下条件的数据库字段:
- phone 和 smsCode 匹配 C 端传入
- expiration 小于/等于当前时间戳
/**
* 验证验证码是否和云数据库中一致
*
* @param {string} phone
* @param {string} smsCode
* @param {object} ctx
* @return {Promise}
*/
async function verifySmsCode(phone, smsCode, ctx) {
const { db, visitTime } = ctx;
const _ = db.command;
const res = await db
.collection(config.collection)
.where({
phone,
smsCode,
expiration: _.gte(visitTime)
})
.get();
return !!res.data.length;
}
清空过期验证码
查询 expiration 过期的所有记录,直接删除数据库记录
代码语言:javascript复制/**
* 清空过期验证码
*
* @param {object} ctx
*/
async function clearSmsCode(ctx) {
const { db, visitTime } = ctx;
const _ = db.command;
await db
.collection(config.collection)
.where({
expiration: _.lt(visitTime)
})
.remove();
}
C 端消费
基于 tcb-js-sdk,通过匿名登录,调用短信验证码的云函数。请参考tcb-js-sdk 文档