前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >云开发系列(一):实现验证码登录

云开发系列(一):实现验证码登录

原创
作者头像
楚歌
修改2020-11-23 00:01:31
3.5K1
修改2020-11-23 00:01:31
举报

作为一个系列的开端,必定是要给自己挖坑的。

终端作为所有用户的真正使用设备,终端开发者也是离用户最近的开发人员,它肩负着将后方提供的一个又一个独立服务整合为体验良好的产品的使命。面对不同的场景,所挑选的后方服务不同,实现方法也不同。

而随着云上产品越来越多,SaaS,BaaS,FaaS的完善,终端开发人员的选择越来越多,这个系列之所以加一个「云」字,是希望在这里以一个终端开发人员的视角,对比在开发目前市面上常见功能的时候,使用传统方案和云上开发(cloudbase)方案的不同之处。

另还准备开另外一个坑,是想要对目前有要趋势的一些SaaS,做SDK接入整合与异常问题分析(目前暂定实时音视频与IM,以后可能有AI之类的,也许吧,又是一个大坑),姑且叫它「云终端系列吧」。

PS. 鄙人技术栈为 JavaScript 系,出身是一个前端,现在也许能称自己是个伪全栈...(萌新瑟瑟发抖)。

以下的介绍均为 Javascript 语言

当然这个坑的第一章要挖的简单一点...

短信验证码登录

逻辑分析

实现一个短信验证码,我们最基本需要以下几个部分

(1)终端登录表单

(2)请求后端服务器

(3)后端服务器请求短信验证码发送短信,并将手机号与验证码的映射关系存于数据库中,并增加一条过期时间字段

(4)前端接受短信,提交完整表单

(5)后端判断是否符合映射关系,判断是否登录成功

听起来好像很简单,但是要从0开发,那就问题多多了...

传统架构

首先你需要一台自己的购买自己的服务器,当然要是放在20年前,你大概得去买一台实体服务器,这就很「传统」,不过为了不为难大家,还是让大家直接从IaaS开始,买一台最简单的云服务器好了。

emmmm看起来还不够,要买台数据库来满足逻辑(3),或者自己在服务器上下载一个数据库

这听起来就很麻烦啊喂!
这听起来就很麻烦啊喂!

噢我的上帝,如果是公网服务器还访问不了数据库,咱们还需要购买一个vpc搞一个私有子网才能访问云上数据库

当然实际上这个业务场景搞个redis应该是最符合场景的

购买云数据库 Redis 实例,具体操作请参见 购买redis数据库。

记得买到同一个地域下面

参数

取值样例

计费模式

按量计费

地域

与服务器同地域

数据库版本

Redis 4.0

架构

标准架构

网络

Demo VPC,Demo 子网

实例名

立即命名:Demo 数据库

购买数量

1

准备工作

(以上来自腾讯云短信服务的需求,其他友商的服务所需都大同小异,因为短信这个东西比较严格)

配置短信服务

步骤1:配置短信内容

短信签名、短信正文模板提交后,我们会在2个小时左右完成审核,您可以 配置告警联系人 并设置接收模板和签名审核通知,便于及时接收审核通知。

步骤1.1:创建签名

  1. 登录 短信控制台
  2. 在左侧导航栏选择【国内短信】>【签名管理】,单击【创建签名】。
  3. 结合实际情况和 短信签名审核标准 设置以下参数:参数取值样例签名用途自用(签名为本账号实名认证的公司、网站、产品名等)签名类型App签名内容测试 Demo证明类型小程序设置页面截图证明上传
  1. 单击【确定】。 等待签名审核,当状态变为【已通过】时,短信签名才可用。

步骤1.2:创建正文模板

  1. 登录 短信控制台
  2. 在左侧导航栏选择【国内短信】>【正文模板管理】,单击【创建正文模板】。
  3. 结合实际情况和 短信正文模板审核标准 设置以下参数:参数取值样例模板名称验证码短信短信类型普通短信短信内容您的注册验证码:{1},请于{2}分钟内填写,如非本人操作,请忽略本短信。
  4. 单击【确定】。 等待正文模板审核,当状态变为【已通过】时,正文模板才可用,请记录模板 ID。

现在,搞前端的同学一定会有萌生如下想法:

好家伙,还没开始写代码呢
好家伙,还没开始写代码呢

不过我们有一个好消息,我们终于要开始..........写服务端业务代码啦!!!

这就是前端吗,可真是太有趣了
这就是前端吗,可真是太有趣了

后端业务代码

以Node为例,以下是长长长长长的代码部分express实现,以下代码的部分仅供参考!,(而且可能写的时候写了啥bug不自知,阿巴阿巴阿巴)

代码语言:txt
复制
const fs = require('fs');
const redis = require('ioredis');
const tencentcloud = require('tencentcloud-sdk-nodejs');
const queryParse = require('querystring')
const expireTime = 5 * 60;//验证码有效期5分钟
const express = require('express')
const app = express();
const http = require('http')
const https = require('https');

//跨域处理
app.all("*",function (req,res,next) {
    
    res.header("Access-Control-Allow-Origin","*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With");
    res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
    res.header("X-Powered-By",' 3.2.1');
    res.header("Content-Type", "application/json;charset=utf-8");

    next();
});
//路由
app.get('/sms', function(req,res,next){
  let queryString = event.query // get形式
  next(queryString);
})

app.post('/sms',function(req,res,next){
  let queryString = queryParse.parse(req.body)
  next(queryString);
}
app.use(function (queryString,req, res) {
  
  if(!queryString || !queryString.method || !queryString.phone) {
    return {
      codeStr: 'InValidParam',
      msg: "缺少参数"
    }
  }
  const redisStore = new redis({
    port: 6369, // Redis instance port, redis实例端口
    host: process.env.REDIS_HOST, // Redis instance host, redis实例host
    family: 4,
    password: process.env.REDIS_PASSWORD, // Redis instance password, redis实例密码
    db: 0
  });
  if(queryString.method === "getSms") {
    return await getSms(queryString, redisStore)
  } else if(queryString.method === "login") {
    return await loginSms(queryString, redisStore)
  }
})

//业务逻辑
/*
* 功能:登录,校验验证码
*/
async function loginSms(queryString, redisStore) {
  if(!queryString.code) {
    return {
        codeStr: 'MissingCode',
        errorMessage: "缺少验证码参数"
    }
  }
  const redisResult = await redisPromise(redisStore, queryString)
  if(!redisResult) {//没有找到记录
    return {
      codeStr: 'CodeHasExpired',
      msg: "验证码已过期"
    }
  }
  let result = JSON.parse(redisResult)
  
  if(!result || result.used || result.num >= 3) {
    return {
      codeStr: 'CodeHasValid',
      msg: "验证码已失效"
    }
  }
  
  if(result.code == queryString.code) { //验证码校验正确
    updateRedis(redisStore, queryString.phone, result, true) //将验证码更新为已使用
    // 验证码校验通过,执行登录逻辑
    console.log('校验验证码成功')
    return {
      codeStr: 'Success',
      msg: '校验验证码成功'
    }
  } else { // 验证码校验失败
    updateRedis(redisStore, queryString.phone, result, false)
    return {
      codeStr: 'CodeIsError',
      msg: "请检查手机号和验证码是否正确"
    }
  }
}
// 更新redis状态
function updateRedis(redisStore, phone, result, used) {
  const sessionCode = {
    code: result.code,
    sessionId: result.sessionId,
    num: ++result.num, //验证次数,最多可验证3次
    used: used //true-已使用,false-未使用
  }
  redisStore.set('sms_' + phone, JSON.stringify(sessionCode));
  if(used) {
    redisStore.expire('sms_' + phone, 0);
  } else {
    redisStore.expire('sms_' + phone, expireTime);
  }
}
/*
 * 功能:根据手机号获取短信验证码
 */
async function getSms(queryString, redisStore) {
  const code = Math.random().toString().slice(-6);//生成6位数随机验证码
  const sessionCode = {
      code: code,
      num: 0, //验证次数,最多可验证3次
      used: false //false-未使用,true-已使用
  }
  redisStore.set('sms_' + queryString.phone, JSON.stringify(sessionCode));
  redisStore.expire('sms_' + queryString.phone, expireTime);

  let queryResult = await sendSms(queryString.phone, code)
  return queryResult
}
/*
 * 功能:通过sdk调用短信api发送短信
 * 参数 手机号、短信验证码
 */
async function sendSms(phone, code) {
  const SmsClient = tencentcloud.sms.v20190711.Client;
  const Credential = tencentcloud.common.Credential;
  const ClientProfile = tencentcloud.common.ClientProfile;
  const HttpProfile = tencentcloud.common.HttpProfile;
  const secretId = TENCENTCLOUD_SECRETID;
  const secretKey = ENCENTCLOUD_SECRETKEY;
  const token = ENCENTCLOUD_SESSIONTOKEN;
  //改为自己的代码

  let cred = new Credential(secretId, secretKey, token);
  let httpProfile = new HttpProfile();
  httpProfile.endpoint = "sms.tencentcloudapi.com";
  let clientProfile = new ClientProfile();
  clientProfile.httpProfile = httpProfile;
  let client = new SmsClient(cred, "ap-guangzhou", clientProfile);

  let req = {
      PhoneNumberSet: ["+" + phone], //大陆手机号861856624****
      TemplateID: process.env.SMS_TEMPLATE_ID, //腾讯云短信模板id
      Sign: process.env.SMS_SIGN, //腾讯云短信签名
      TemplateParamSet: [code],
      SmsSdkAppid: process.env.SMS_SDKAPPID //短信应用id
  }
  
  let queryResult = await smsPromise(client, req)
  return queryResult
}

async function smsPromise(client, req) {
  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({
                      codeStr: response.SendStatusSet[0].Code,
                      msg: response.SendStatusSet[0].Message
                  })
              } else {
                  resolve({
                      codeStr: response.SendStatusSet[0].Code,
                      msg: response.SendStatusSet[0].Message
                  })
              }
          }                
      });
  })
}

async function redisPromise(redisStore, queryString) {
  return new Promise((res, rej) => {
    redisStore.get('sms_' + queryString.phone, function (err, result) {
      if (err) {
        rej(err)
      }
      res(result)
    });
  })
}

//服务启动

let httpServer = http.createServer(app);
let httpsServer = https.createServer({
    key: fs.readFileSync('./cert/privatekey.pem', 'utf8'), 
    cert: fs.readFileSync('./cert/certificate.crt', 'utf8')
}, app);

let PORT = 80;
let SSLPORT = 443;

httpServer.listen(PORT, function() {
    console.log('HTTP Server is running on: http://localhost:%s', PORT);
});
httpsServer.listen(SSLPORT, function() {
    console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT);
});

太快乐了,一个前端到现在还在看着后端写代码呢,wow~

这就是个年轻前端的发言
这就是个年轻前端的发言

事实上,在云端这么发达的今天,加上V8引擎和Node.js的快速发展,这些功能从组织架构上确实不一定由前端做,但是一个前端可以也应该去学会这些与服务器,数据库交互的写法,只会构建UI界面和交互的前端终究在时代里会被慢慢淘汰,而未来的前端应叫做「大前端」或者「终端」,请各位同学耗子尾汁~

前端代码

好的那么终于到前端的代码了,这里就写个vue的组件吧,如果有需要大家自己改成自己需要的哈,样式就用ElementUI,请求用axios

代码语言:txt
复制
//vue<script src="//unpkg.com/vue/dist/vue.js"></script>
<template>
  <div id="app">
    <el-form :model="form" class="demo-form-inline">
      <el-form-item label="手机号">
        <el-input v-model="form.phone" placeholder="手机号"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getCode" :disable="this.buttonTime>0?'disabled':'false'’">
        获取验证码{{this.buttonTime <= 0?"":this.buttonTime}}
        </el-button>
      </el-form-item>
      <el-form-item label="验证码">
         <el-input v-model="form.code" placeholder="验证码"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handlelogin">登录</el-button>
      </el-form-item>
    </el-form>
  </div>
</template>
<script>
import axios from 'axios';
import elementUI from 'element-ui'
export default {
    data() {
      return {
        form: {
          phone: '',
          code: '',
          buttonTime:0 //按钮是否可以按
        }
      }
    },
    methods: {
      handleLogin() {
        const host = ....;
        //get
        axios.get(`/${host}/sms?phone=${this.phone}&code=${this.code}`)
          .then(function (response) {
          console.log(`登陆成功,回调为:${response`);
        })
        .catch(function (error) {
          console.log(`登陆失败,失败信息为:${error}`)
        });
        //post
        axios.post(`/${host}/sms`, {
          phone: this.phone,
          code: this.code
        })
        .then(function (response) {
          console.log(`登陆成功,回调为:${response`);
        })
        .catch(function (error) {
          console.log(`登陆失败,失败信息为:${error}`)
        });
      },     
      getCode(){
         const host = ....;
         setButtonDisabled();
         axios.get(`/${host}?phone=${this.phone}`)
          .then(function (response) {
          console.log(`登陆成功,回调为:${response`);
        })
        .catch(function (error) {
          console.log(`登陆失败,失败信息为:${error}`)
        });
        //post
        axios.post('/${host}', {
          phone: this.phone,
        })
        .then(function (response) {
          console.log(`发送成功,回调为:${response}`);
        })
        .catch(function (error) {
          console.log(`发送失败,失败信息为:${error}`)
        });
      },
      setButtonDisable(){
       this.buttonTime = 60;
         let timer = setInterval(()=>{
             if(this.buttonTime == 0){
                clearInterval(timer)
                return;
             }
             this.buttonTime--;
         },1000)
      }
     
  }
}
</script>

经过一番辛苦的折腾,咱们终于能把两端的代码写完了。

但是呢,写完 ≠ 跑通,虽然我们在本地启动node服务后可以在localhost层面上进行测试,但是要部署还有很多步骤

(1)首先我们使用Putty或者FileZilla这样的产品,将服务和编译后的前端静态文件部署到服务器上

FileZilla参考图
FileZilla参考图

(2)在云服务器内node启动服务,若想永久启动,可以npm下载pm2或forever

(3)之后访问静态文件的主页,就可以正常访问了

(4)如果你需要域名,或者需要ssl证书的话,又要购买其他产品并走相应的流程...

有没有简单的方法?

看到这里你是不是觉得很麻烦,就算我们简洁一点,把后端服务换成FaaS,去用云函数替代,这个部分也就是后端业务部署的部分简单了一些,这里对redis等配置,处理都还没有列出讲解(因为这毕竟是开发的文章,并不想花重大笔墨去阐述如何配置数据库,Nginx之类的,这已经有很多成熟的文章介绍了)。

所以对于一个开发人员而言,尤其是终端开发人员,编写与用户直接相关的代码(前端交互,接口逻辑)才是关键,但是事实上,如果我们真要用传统的方式来一遍流程,大量的时间开销会放在数据库、服务器、备案、证书等非业务逻辑上的东西,这并不是我们期待看到的。

所以为了解决这种难点,体现我们真正意义上的「云开发」,我们推荐cloudbase。也就是整体上云,采用云原生架构开发

云原生架构开发

cloudbase是什么

  • cloudbase 是Serverless 云原生一体化产品方案,助力小程序、Web应用、移动应用成功。它的用户群体是开发者,目前主要的群体是小程序开发者和Web开发者,跨端开发目前uniapp和我们有合作。其他终端的有.NET 和 Flutter,小游戏有cocos
  • 可以理解为是开发者的一个工具箱,就像家里常备的那种工具箱,产品的愿景,是希望所有开发者能够拎着这个名为cloudbase的工具箱,快速的使用云上能力构建出所需要的应用,进行敏捷开发
一个开发者也应该拥有这样的开发工具箱
一个开发者也应该拥有这样的开发工具箱

这个的产品的具体内容可以看产品文档,这里只教怎么用

用cloudbase实现短信验证码

(0)配置腾讯云短信服务,这个都是要做的

(1)构建前端代码

  • 可以用cloudbase提供的开发者vscode插件直接在vscode里面构建
一键构建主流的所有应用
一键构建主流的所有应用
  • 前端代码部分大同小异,主要是调用短信验证服务的方式变了(环境ID是创建cloudbase环境后自动分配的)
代码语言:txt
复制
const cloudbase = require("@cloudbase/js-sdk");
const extSms = require("@cloudbase/extension-sms");

const app = cloudbase.init({
  env: "您的环境ID"
});

cloudbase.registerExtension(extSms);

demo();

async function demo() {
  try {
    let phone = ""; // 输入用户手机号
    // 发送短信验证码
    await cloudbase.invokeExtension(extSms.name, {
      action: "Send",
      app,
      phone
    });

    let smsCode = ""; // 用户填写验证码
    // 验证码校验
    await cloudbase.invokeExtension(extSms.name, {
      action: "Verify",
      app,
      phone,
      smsCode
    });

    // 验证码登录
    await cloudbase.invokeExtension(extSms.name, {
      action: "Login",
      app,
      phone,
      smsCode
    });
    console.log("登录成功,目前是正式用户");
  } catch (err) {
    console.log(JSON.stringify(err, null, 4));
  }
}

这里我们重点关注 invokeExtension 这个API,这个API可以直接调用短信服务,你会惊讶的发现,好像我的前端就可以直接调用服务了一样,以前需要经过Node层转发。

是的这就是大前端时代下的一个体现,功能的接口调用直接被封装在前端SDK中,供开发者直接调用,那么这个调用的功能在哪呢?不会我们又要购买什么服务器数据库才能调吧?

No No No!我们只需要轻轻的在这里点一下安装就好了

image.png
image.png

然后?

然后就可以调用了宝贝儿!什么node,什么服务器启动都见鬼去吧!

你问我那部署咋办,我没买服务器我FileZilla传哪里呢?

。。。。。

同学你这个问题问的非常好

我们确实没有办法部署到服务器上

因为

我们只需要在这里点一下上传文件夹,把打包好的静态文件上传并在配置页面配置一下索引文档就好了呀~

image.png
image.png

cloudbase也提供了一个默认域名供给访问,如果你有自己的域名的话还可以配置上安全域名

云开发的核心是将所有的精力都放在开发者关心的功能与业务代码上

如果您看到了这里,麻烦点个赞吧,这对我真的很重要~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 短信验证码登录
    • 逻辑分析
    • 传统架构
      • 准备工作
        • 配置短信服务
          • 步骤1:配置短信内容
            • 步骤1.1:创建签名
            • 步骤1.2:创建正文模板
          • 后端业务代码
            • 前端代码
              • 有没有简单的方法?
              • 云原生架构开发
                • cloudbase是什么
                  • 用cloudbase实现短信验证码
                  相关产品与服务
                  云开发 CloudBase
                  云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档