云函数(Serverless Cloud Function,SCF)是腾讯云为企业和开发者们提供的无服务器执行环境,帮助我们在无需购买和管理服务器的情况下运行代码。 本文会演示使用serverless云函数开发一个短信验证码登录注册服务。
传统开发协作流程(多角色参与): 1.后台开发:短信接口发送短信API、校验短信验证码是否有效API、存储用户验证状态 2.运维开发:接口部署,容灾 3.前端(客户端)开发:前端逻辑开发(调用接口,查询状态等)
传统开发模式的问题:多角色参与、工作量大、维护成本高
Serverless云函数开发模式:全流程基本上可一个人完成所有功能
这里是我们要实现的短信验证码登录的流程图,主要涉及到serverless云函数开发、短信服务、云db存储用户信息。
准备工作
- 已 注册腾讯云 账号,并完成 企业实名认证。
- 已 购买 短信套餐包。
- 准备短信签名归属方资质证明文件,详细的文件清单以及规范请参见 签名审核标准。 本文以使用企业营业执照作为资质证明文件为例。
- 了解短信正文内容审核规范,详情请参见 正文模板审核标准。
- 已获取短信应用的 SDKAppID。
相关资料
- Demo 源码
- 其他产品文档
- 私有网络产品文档
- 云数据库 MySQL 产品文档
- NAT 网关产品文档
- 云函数产品文档
步骤1:配置短信内容
短信签名、短信正文模板提交后,我们会在2个小时左右完成审核,您可以 配置告警联系人 并设置接收模板和签名审核通知,便于及时接收审核通知。
步骤1.1:创建签名
- 登录 短信控制台。
- 在左侧导航栏选择【国内短信】>【签名管理】,单击【创建签名】。
参数 | 取值样例 |
---|---|
签名用途 | 自用(签名为本账号实名认证的公司、网站、产品名等) |
签名类型 | APP |
签名内容 | 测试 Demo |
证明类型 | 小程序设置页面截图 |
证明上传 |
- 单击【确定】。 等待签名审核,当状态变为【已通过】时,短信签名才可用。
步骤1.2:创建正文模板
- 登录 短信控制台。
- 在左侧导航栏选择【国内短信】>【正文模板管理】,单击【创建正文模板】。
参数 | 取值样例 |
---|---|
模板名称 | 验证码短信 |
短信类型 | 普通短信 |
短信内容 | 您的注册验证码:{1},请于{2}分钟内填写,如非本人操作,请忽略本短信。 |
- 单击【确定】。 等待正文模板审核,当状态变为【已通过】时,正文模板才可用,请记录模板 ID。
步骤2:设置短信发送频率限制(可选)
!个人认证用户不支持修改频率限制,如需使用该功能,请将 “个人认证” 变更为 “企业认证”,具体操作请参见 实名认证变更指引。
为了保障业务和通道安全,减少业务被刷后的经济损失,建议 设置发送频率限制。另外,您也可以结合使用 腾讯云验证码 以便最大程度地保护业务安全。
本文以短信的默认频率限制策略为例。
- 同一号码同一内容30秒内最多发送1条。
- 同一手机号一个自然日最多发送10条。
步骤3:配置私有网络和子网
默认情况下,云函数部署在公共网络中,只可以访问公网。如果开发者需要访问腾讯云的 TencentDB 等资源,需要建立私有网络来确保数据安全及连接安全。
- 按需 规划网络。
参数 | 取值样例 |
---|---|
所属地域 | 华南地区(广州) |
名称 | Demo VPC |
IPv4 CIDR | 10.0.0.0/16 |
子网名称 | Demo 子网 |
IPv4 CIDR | 10.0.0.0/16 |
可用区 | 广州三区 |
步骤4:配置 MySQL 数据库
云数据库 MySQL 实例需与 步骤3 配置私有网络的地域和子网的可用区保持一致。
参数 | 取值样例 |
---|---|
计费模式 | 按量计费 |
地域 | 广州 |
数据库版本 | MySQL5.7 |
架构 | 高可用版 |
主可用区 | 广州三区 |
备可用区 | 广州四区 |
实例规格 | 4核8000MB |
硬盘 | 200GB |
网络 | Demo VPC,Demo 子网 |
实例名 | 立即命名:Demo 数据库 |
购买数量 | 1 |
参数 | 取值样例 |
---|---|
支持字符集 | UTF8 |
表名大小写敏感 | 是 |
自定义端口 | 3306 |
设置 root 帐号密码 | 请自定义设置 |
确认密码 | 请再次输入密码 |
- 登录 MySQL 数据库,具体操作请参见 登录 phpMyAdmin。
- 根据实际需求,创建数据表和字段用于存储用户的手机号、头像、用户昵称等信息,具体操作请参见 创建数据库和表。
步骤5:新建云函数
云函数目前支持 Python、Node.js、PHP、Java 以及 Golang 语言开发,本文以 Node.js 为例。
参数 | 取值样例 |
---|---|
函数名称 | Demo |
运行环境 | Nodejs 8.9 |
创建方式 | 模板函数:helloworld |
- 部署函数并配置触发方式为【API网关触发器】,具体操作请参见 部署函数。
步骤6:配置 NAT 网关
部署在 VPC 中的云函数默认隔离外网。若想使云函数同时具备内网访问和外网访问能力,可通过以下两种方式实现:
- 通过配置云函数公网访问能力,且公网访问可控制出口地址唯一,请参考 固定公网出口 IP。
- 通过 VPC 添加 NAT 网关,请参考 私有网络中配置 NAT。
本文以添加 NAT 网关为例。
- 新建 NAT 网关,具体操作请参考 私有网络中配置 NAT。
- NAT 网关要和函数、VPC 部署在同一地域。
- NAT 网关的所属网络需要选择函数所在的 VPC。
参数 | 取值样例 |
---|---|
网关名称 | Demo NAT |
所属网络 | Demo VPC |
网关类型 | 小型 |
出带宽上限 | 100Mbps |
弹性 IP | 新建弹性 IP |
- 根据实际需求创建路由策略,具体操作请参考 私有网络中配置 NAT。
步骤7:部署短信 SDK
- 执行以下命令,安装 SDK。npm install tencentcloud-sdk-nodejs --save
- 在代码中引用短信模块代码。
- 配置发送短信核心逻辑。
/**
*
* @param {*} 功能:通过 SDK 发送短信
* @param {*} 参数:手机号、短信验证码
*/
async function sendSms(phone, code) {
const tencentcloud = require('tencentcloud-sdk-nodejs');
const SmsClient = tencentcloud.sms.v20190711.Client;
const Credential = tencentcloud.common.Credential;
const ClientProfile = tencentcloud.common.ClientProfile;
const HttpProfile = tencentcloud.common.HttpProfile;
//腾讯云账户 secretId,secretKey,切勿泄露
const secretId = "secretId";//需要配置为真实的 secretId
const secretKey = "secretKey";//需要配置为真实的 secretKey
let cred = new Credential(secretId, secretKey);
let httpProfile = new HttpProfile();
httpProfile.endpoint = "sms.tencentcloudapi.com";
let clientProfile = new ClientProfile();
clientProfile.httpProfile = httpProfile;
let client = new SmsClient(cred, "ap-guangzhou", clientProfile);
phone = "+86" + phone;//国内手机号
let req = {
PhoneNumberSet: [phone],//发送短信的手机号
TemplateID: "",//<a href="#Step1_2">步骤1.2</a> 中创建并记录的模板 ID
Sign: "",//<a href="#Step1_1">步骤1.1</a> 中创建的签名
TemplateParamSet: [code],//随机验证码
SmsSdkAppid: ""//短信应用 ID
}
function smsPromise() {
return new Promise((resolve, reject) => {
client.SendSms(req, function (errMsg, response) {
if (errMsg) {
reject(errMsg)
} else {
if (response.SendStatusSet && response.SendStatusSet[0] && response.SendStatusSet[0].Code === "Ok") {
resolve({
errorCode: 0,
errorMessage: response.SendStatusSet[0].Message,
data: {
codeStr: response.SendStatusSet[0].Code,
requestId: response.RequestId
}
})
} else {
resolve({
errorCode: -1003,//短信验证码发送失败
errorMessage: response.SendStatusSet[0].Message,
data: {
codeStr: response.SendStatusSet[0].Code,
requestId: response.RequestId
}
})
}
}
});
})
}
let queryResult = await smsPromise()
return queryResult
}
步骤8:检验验证码核心逻辑
验证码的时效性要求较高,您可以把验证码存在内存中或存在云数据库 Redis 中。以手机号作为 key,存储发送时间、验证码、验证次数、是否已验证过等信息。出于安全考虑,建议设置防止暴力破解的限制,本文以验证码最多验证3次为例。
/* * 功能:根据手机号获取短信验证码 */ async function getSms(queryString) { const code = Math.random().toString().slice(-6);//生成6位数随机验证码 const sessionId = Math.random().toString().slice(-8);//生成8位随机数 const sessionCode = { code: code, sessionId: sessionId, sendTime: new Date().getTime(), num: 0,//验证次数,最多可验证3次 used: 1//1-未使用,2-已使用 } clearCacheCode()
cacheCode[queryString.phone] = sessionCode
步骤9:配置登录模块
登录模块主要用于用户注册或登录,首次登录(即注册)时将保存用户的手机号、用户名、头像、注册时间等信息。
/*
功能:登录
*/
async function loginSms(queryString) {
const connection = mysql.createConnection({
host: '', // The ip address of cloud database instance, 云数据库实例 IP 地址
user: '', // The name of cloud database, for example, root, 云数据库用户名,例如 root
password: '', // Password of cloud database, 云数据库密码
database: '' // Name of the cloud database, 数据库名称
});
connection.connect();if(queryString.token) {
return await verifyToken(connection, queryString)
}if(!queryString.code || !queryString.sessionId) {
return {
errorCode: -1001,
errorMessage: "缺少参数"
}
}let result = cacheCode[queryString.phone]
if(!result || result.used === 2 || result.num >= 3) {
return {
errorCode: -1100,
errorMessage: "验证码已失效"
}
}
if(result.sessionId !== queryString.sessionId) {
return {
errorCode: -1103,
errorMessage: "sessionId不匹配"
}
}if(result.code == queryString.code) {
cacheCode[queryString.phone].used = 2;//将验证码更新为已使用
const queryInfoSql =select * from info where phone = ?
let queryInfoResult = await wrapPromise(connection, queryInfoSql, [queryString.phone])
if(queryInfoResult.length === 0) {//没有找到记录,未注册
return await generateInfo(connection, queryString)
} else {
let infoResult = queryInfoResult[0]
return {
errorCode: 0,
errorMessage: "登录成功",
data: {
phone: infoResult.phone,
token: getToken(infoResult.userId, infoResult),
name: infoResult.name,
avatar: infoResult.avatar,
userId: infoResult.userId.toString()
}
}
}
} else {
updateCacheCode(queryString.phone, result)
return {
errorCode: -1102,
errorMessage: "验证码错误,请重新输入"
}
}
}
另外,为了登录更便捷,您可以通过 Json web token 标准来生成 token 维护登录状态,实现短时间内登录无需短信验证码的功能。
/*
功能:利用 json web token 签发一个 token
*/
function getToken(userId, infoResult) {
return jwt.sign({
phone: infoResult.phone,
userId: userId,
name: infoResult.name,
avatar: infoResult.avatar
}, privateKey, {expiresIn: tokenExpireTime});
}