同学们好!在《我家租房网》应用的注册流程中,用户将在图片验证码中看到的文本,输入到注册表单的“图片验证码”编辑框中,并在填好手机号后,按下“获取短信验证码”按钮。这时,浏览器会将图片ID、用户输入的图片验证码文本和手机号一起发送给服务器。服务器先对用户输入的图片验证码进行校验,一旦校验通过,即随机生成一个短信验证码,根据用户的手机号,通过短信平台发送到用户的手机上,同时将手机号和短信验证码,以键值对的形式保存起来。我们这节课的主要任务,就是实现短信验证码的生成、发送和保存。


直觉告诉我们,发送短信需要借助电信运营商所提供的接口。但事实上,电信运营商所提供的短信接口,通常是不对公众开放的,而只开放给象华为云、腾讯云、阿里云这样的云服务厂商。这些厂商负责搭建短信开发平台,以远程API的形式,为一般用户提供短信接入服务。这里,我们选择的是,阿里云提供的短信平台。因此,我们首先需要注册一个阿里云账号,并购买相应的短信服务。为了调用短信平台的远程API,我们还需要安装阿里云SDK,并创建访问密钥。


使用阿里云短信平台发送测试短信,需要绑定测试号码。在完成所有这些准备工作后,我们将尝试在Go语言程序中,调用短信平台的接口,向特定的手机号,发送短信验证码。如果我们的短信验证码发送成功,接着就可以将这部分功能集成到前端服务器中,通过浏览器操作发送短信。最后再将所有这些功能移植到后端微服务中,并在前端添加对后端微服务的远程调用。


我们先简要了解一下,目前都有哪些短信开发平台可供选择?


事实上,象聚合数据、京东万象、华为云,这些短信服务提供商,都是属于用户数比较多,口碑也很不错的平台,当然价格也会相对高一些。感兴趣的同学可以去它们的官网,了解更多详细信息。


而象腾讯云、阿里云,这些短信开发平台,服务质量也很好,而且价格更亲民。尤其是阿里云,不仅服务器的整体配置较高,性能卓越,而且生态更加完善,面向各种编程语言的接口、文档和例程十分丰富,对开发者非常友好。


为了使用阿里云提供的短信开发平台,需要先注册阿里云账号,成为它的合法用户。


如果您已经是支付宝用户了,那么其实您就已经拥有阿里云账号了。直接在阿里云官网,用支付宝扫码,即可完成登录。


当然,拥有阿里云账号,并不意味着就可以使用它的短信服务了。和所有短信开发平台一样,阿里云提供的短信服务也是需要付费才能使用的。


您可以直接进入阿里云的短信服务首页,根据您的经济实力,购买不同的短信套餐包产品。阿里云会不定期地推出一些打折促销活动。目前,在不考虑各种优惠政策的前提下,国内通用短信套餐包的价格是,50元人民币可发1000条短信,有效期一年。另外需要注意,面向个人用户的短信服务,只能发送短信验证码和短信通知,不能发送推广短信和群发短信。不过这对于我们的《我家租房网》应用而言,已经足够了。


通过手机发送短信,只需将短信内容和对方手机号,发送给电信运营商即可。而通过短信开发平台发送短信,则需要调用平台提供的远程接口,接口参数中包括短信内容和对方手机号。为了简化对远程接口的调用,阿里云为应用程序的开发者,提供了非常简单的SDK,可以象调用任何本地函数一样,调用阿里云平台的远程接口。


我们可以直接从GitHub上下载阿里云SDK的软件包,并安装到正确的路径下。


在使用阿里云SDK时,需要提供访问密钥,为此我们需要创建该密钥。


进入阿里云SDK客户端文档页面,点击“用户信息管理”超链接。


在弹出的“安全提示”对话框中,点击“继续使用AccessKey”按钮。


点击位于用户访问密钥列表右上角的“创建AccessKey”按钮。


在弹出的“手机验证”对话框中,获取并填写校验码,点击“确定”按钮。


在弹出的“新建用户AccessKey”对话框中,点击“保存AK信息”按钮。


浏览器会为您下载一个扩展名为“.csv”的文件。可以用WPS、Excel等电子表格软件,或记事本等文本编辑器,打开该文件。文件中包含访问密钥的ID和密钥密文。在后面调用阿里云SDK所提供的接口时,需要提供这两个信息。


为了防止短信发送功能被滥用,平台对测试用手机号码需要进行绑定,即只能向被绑定的手机号发送短信。


再次进入阿里云的短信服务首页,点击位于左边栏顶部的“快速学习”目录项。


点击快速学习页面中的“绑定测试手机号码”按钮,输入期望被绑定的手机号码,获取并填写验证码,点击“确定”按钮。这时可以看到,被绑定的手机号码,已经出现在测试手机号码列表中。


在完成前面这些准备工作以后,接下来我们将动手编写一个测试程序,尝试一下在Go语言程序中借助短信开发平台,向特定手机号码发送短信的操作步骤。


我们在front-end工程目录下的test子目录中创建一个名为sms.go的文件:

在这段代码中,我们首先通过阿里云SDK包里的NewConfig函数,创建一个配置对象,然后通过阿里云SDK证书包里的NewAccessKeyCredential函数,创建一个证书对象,提供给该函数的参数,就是我们先前已经创建并下载的,访问密钥ID和密钥密文。得到这两个对象后,我们就可以以之作为参数,调用阿里云SDK短信接口包里的NewClientWithOptions函数,创建一个客户机对象。该函数的第一个参数是地区ID,这里使用的是“cn-hangzhou”。有了客户机对象,我们就可以向阿里云服务器发送短信业务请求。调用短信接口包里的CreateSendSmsRequest函数,创建一个请求对象,并在请求对象中填入协议、签名名称、模板代码、对方手机号码,和JSON字符串形式的模板参数,其中包含短信验证码的具体内容。用该请求对象作为参数,调用客户机对象的SendSms方法,完成短信发送。该方法返回的响应对象中,包含响应代码和响应消息。


直接在test目录下通过“go run”命令启动测试程序。我们的手机收到了内容为“740523”的短信验证码。


下一步,我们将把测试程序中用于发送短信验证码的代码,集成到前端服务器中。


首先,我们需要关注一下,有关获取短信验证码的页面请求。


启动Consul服务器、GetImageCode后端微服务和前端服务器。从浏览器进入注册页面,填入手机号和图片验证码,点击“获取验证码”。


在调试窗口中,我们可以看到获取短信验证码的页面请求。


获取短信验证码的页面请求中,包含了用户的手机号、用户输入的图片验证码文本和图片ID。服务器所要做的工作是从Redis数据库中,以图片ID为键,读取其值,即正确的图片验证码文本,以此校验用户的输入是否正确。如果正确,则向用户的手机发送一条随机生成的短信验证码。


这里我们要为获取短信验证码添加一条路由,GET方法结合/api/v1.0/smscode/:mobile路径,处理函数名为GetSMSCode。


在front-end工程目录下的main.go文件中添加一条路由:

我们首先通过路由对象的Group方法,创建了一个路由分组,其参数为一段共有的路由路径,后面的路由设置,均为组内路由,可以省略这段共有路径。接着我们又调用了路由分组对象的GET方法,为获取短信验证码添加了一条路由,将GET方法结合/smscode/:mobile路径,路由到controller包的GetSMSCode函数。将GetSMSCode函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。


当然,在controller包里真的得有GetSMSCode函数。


为此,我们在front-end工程目录下controller子目录中的user.go文件中定义GetSMSCode函数:

在GetSMSCode函数里,我们直接调用了Gin框架调用此函数时传入的上下文对象的Param方法,取添加路由时放在请求路径中的占位符“mobile”为参数,接收其返回的手机号。类似地,我们还可以通过该上下文对象的Query方法,从请求URL的参数部分,获取图片验证码文本和图片ID。


为了校验用户输入的图片验证码是否正确,这里需要从Redis数据库中,以图片ID为键,读取其值,即正确的图片验证码文本。


我们在front-end工程目录下model子目录中,添加一个名为redis.go的文件,封装所有与操作Redis数据库有关的代码:

这里我们定义了一个名为ReadImageCode的函数,用于从Redis数据库中读取图片验证码文本。该函数只带有一个输入参数,uuid即图片ID。首先调用Redigo的Dial函数,连接数据库,同时获得用于后续数据库操作的连接对象。然后通过连接对象的Do方法,以图片ID为键,读取其值,并将其类型具体化为字符串,即图片验证码文本。该函数返回图片验证码文本和访问Redis过程中可能得到的错误对象。


下面我们来编写实际发送短信验证码的代码。


回到user.go文件中的GetSMSCode函数,添加实际发送短信验证码的代码:

在从请求URL中依次获取手机号、用户输入的图片验证码文本和图片ID后,创建一个用于承载响应信息的映射。首先,根据图片ID,从Redis数据库中读取正确的图片验证码文本,与用户输入的图片验证码文本进行比较,二者不等,说明用户输入的图片验证码有误,设置错误响应并返回。图片验证码校验通过,则执行与sms.go中类似的代码,向用户的手机发送短信验证码。与sms.go中的做法不同的是,这里的手机号取自用户的输入,而验证码为随机生成的6位整数。如果发送失败,或者阿里云服务器响应失败,则设置错误响应并返回。如果一切顺利,则向浏览器响应成功。


因为我们前面已经启动了Consul服务器和GetImageCode后端微服务,这里只需重启前端服务器。在注册页面填入手机号和图片验证码,点击“获取验证码”。这时页面上“获取验证码”的区域会显示倒计时,同时手机会收到一条验证码短信。


截至目前,每次访问Redis数据库之前,我们都要调用Redigo的Dial函数,建立与数据库的连接,并在访问结束后,通过连接对象的Close方法关闭该连接。这显然并非一种高效的操作模式。频繁的建立和关闭连接,本身就是一种低效且耗时的工作。事实上,Redigo还提供了针对Redis数据库连接的池化管理机制,即Redis连接池。借助连接池,可以显著降低建立和关闭数据库连接的频次,提高程序的运行性能。


Redigo中的Redis连接池是一个类型为Pool的对象,其中包括一系列属性。比如MaxIdle代表最大空闲连接数,MaxActive代表最大活动连接数,MaxConnLifetime代表最大连接生命周期,IdleTimeout代表空闲超时,Dial属性是一个回调函数,将在需要创建数据库连接时被调用。我们所要做的工作,只是创建这样一个Pool类型的连接池对象,根据情况设置其中的属性,每当需要用于操作Redis数据库的连接对象时,直接调用连接池对象的Get方法即可。


再次打开front-end工程目录下model子目录中的redis.go文件,修改其中的代码:

这里我们首先创建了一个Pool类型的连接池对象,并对其最大空闲连接数、最大活动连接数、最大连接生命周期、空闲超时和连接回调等属性,做必要的初始化。在后面的ReadImageCode函数中,不再显式调用Redigo的Dial函数建立与数据库的连接,而是通过连接池对象的Get方法获取连接对象。至于这个连接对象是在此刻创建的,还是先前创建恰在此时空闲的,都无所谓,连接池会把这个问题处理得很好。


定义在user.go文件中的GetSMSCode函数,现在确实已经可以向用户的手机发送短信验证码了,但根据我们在前面关于用户注册流程的描述,该函数还应该可以将用户的手机号和短信验证码,以键值对的形式保存到Redis数据库中。


为此,我们首先在前端model目录下的redis.go文件中,添加一个名为SaveSMSCode的函数:

在这段代码中,我们首先从Redis连接池中获取连接对象,然后通过该对象的Do方法,将从参数传入的用户手机号和短信验证码,以键值对的形式保存到Redis数据库中,并返回该方法返回的错误对象。


然后,我们还需要修改前端controller目录下的user.go文件,在GetSMSCode函数中添加对model包SaveSMSCode函数的调用:

在完成短信验证码的发送之后,调用model包的SaveSMSCode函数,将用户的手机号和短信验证码,保存到Redis数据库中。


重启前端服务器。在注册页面填入手机号和图片验证码,点击“获取验证码”。在手机收到短信验证码的同时,Redis数据库中记录了手机号——短信验证码键值对。


我们在前端服务器中,成功地实现了获取短信验证码的业务逻辑。这令我们备受鼓舞,但作为一个前后端分离的项目,这种纯业务性的功能,放在前端服务器中是不合适的。因此我们有必要将与获取短信验证码有关的操作,移植到后端服务器中,且用一个独立的微服务予以实现。


首先,我们来创建一个这样的微服务。


我们可以直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,所创建的微服务名为GetSMSCode。


接着,需要修改一下go-micro为我们自动生成的接口描述。


在back-end工程目录下的GetSMSCode微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为GetSMSCode.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:

这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,其中包含三个字符串,分别表示用户的手机号、用户输入的图片验证码文本和图片ID。CallResponse表示后端返回给前端的响应数据,其中包含两个字符串,错误代码和错误描述。


有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。


为此,我们在GetSMSCode微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。


前面我们曾经讲过,go-micro自动生成的微服务代码框架,默认使用的服务发现是MDNS,而如果我们想改用Consul,就需要修改部分代码。与此同时,我们还需要为微服务进程,设置绑定的IP地址和端口号。


为此,我们需要修改一下GetSMSCode微服务目录中的main.go文件:

我们在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9002端口。


我们已经在前端服务器的模型层,编写了与Redis数据库访问有关的代码。现在需要将其移植到后端微服务中。


我们在GetSMSCode微服务目录下,创建一个名为model的子目录,表示该微服务的模型层。在该子目录中创建redis.go文件,封装所有与操作Redis数据库有关的代码:

这段代码和我们之前为前端服务器编写的代码几乎完全一样。在全局域定义一个Redis连接池对象。ReadImageCode函数用于根据图片ID,从Redis数据库中读取图片验证码文本。SaveSMSCode函数用于将用户的手机号和短信验证码,以键值对的形式保存到Redis数据库中。


后端微服务有时会用到和前端服务器一样的实用工具,比如有关错误代码和错误描述的定义,或者类似StrError这种,将错误代码转换为错误描述的工具函数。


我们可以直接将front-end工程目录下的utils子目录,原封不动地复制到back-end工程目录下的GetSMSCode微服务目录中。


下一步,我们要进入微服务的关键部分,即实现业务逻辑。


打开GetSMSCode微服务目录下,handler子目录中的GetSMSCode.go文件,在其中的Call方法里,添加与获取短信验证码有关的操作:

这段代码与它的前端版本大体一致。只是在这里,用户的手机号、用户输入的图片验证码文本和图片ID,均来自前端服务器传送过来的CallRequest请求对象,而非浏览器页面提交的请求URL。同时将错误代码、错误描述等需要回传给前端服务器的响应数据,放到CallResponse响应对象中。


微服务形式的后端服务器已然就绪,我们可以直接启动它。


启动GetSMSCode微服务,用浏览器访问localhost:8500。我们可以看到GetSMSCode微服务处于健康状态。


最后一步,我们还需要在前端服务器的路由处理函数中,调用后端微服务中的远程方法。


前端和后端共用同一套接口描述。因此,这里我们将GetSMSCode微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetSMSCode目录中。


打开front-end工程目录下controller子目录中的user.go文件。在该文件的import部分增加一行:

修改其中的路由处理函数GetSMSCode,将之前发送短信验证码的代码,替换为调用微服务远程方法的代码:

这段代码通过Consul服务发现,获取了有关名为getsmscode的微服务的信息,远程调用了其中的GetSMSCode对象的Call方法。该方法以包含用户手机号、图片验证码文本和图片ID等信息的请求对象为参数,并返回包含错误代码和错误描述的响应对象。最后,将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。


重启前端服务器。在注册页面填入手机号和图片验证码,点击“获取验证码”。在手机收到短信验证码的同时,Redis数据库中记录了手机号——短信验证码键值对。


谢谢大家,我们下节课再见!