同学们好!在《我家租房网》应用的注册流程中,用户将在图片验证码中看到的文本,输入到注册表单的“图片验证码”编辑框中,并在填好手机号后,按下“获取短信验证码”按钮。这时,浏览器会将图片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的文件:
x1package main
2
3import (
4 "fmt"
5
6 "github.com/aliyun/alibaba-cloud-sdk-go/sdk"
7 "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
8 dysmsapi "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
9)
10
11func main() {
12 config := sdk.NewConfig()
13 credential := credentials.NewAccessKeyCredential(
14 "LTAI5tFFwAp6n28yon5iqazV", "7nZ8PV1CTv7uN8VcRqbwnDG2Z1bJHw")
15 client, err := dysmsapi.NewClientWithOptions(
16 "cn-hangzhou", config, credential)
17 if err != nil {
18 fmt.Println("credentials.NewAccessKeyCredential错误:", err)
19 return
20 }
21
22 request := dysmsapi.CreateSendSmsRequest()
23 request.Scheme = "https"
24 request.SignName = "阿里云短信测试"
25 request.TemplateCode = "SMS_154950909"
26 request.PhoneNumbers = "13552654195"
27 request.TemplateParam = "{\"code\": \"740523\"}"
28
29 response, err := client.SendSms(request)
30 if err != nil {
31 fmt.Println("Client.SendSms错误:", err)
32 return
33 }
34
35 fmt.Println(response.Code, response.Message)
36}
在这段代码中,我们首先通过阿里云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文件中添加一条路由:
xxxxxxxxxx
121...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 apiv10 := router.Group("/api/v1.0")
7 apiv10.GET("/session", controller.GetSession)
8 apiv10.GET("/imagecode/:uuid", controller.GetImageCode)
9 apiv10.GET("/smscode/:mobile", controller.GetSMSCode)
10 ...
11}
12...
我们首先通过路由对象的Group方法,创建了一个路由分组,其参数为一段共有的路由路径,后面的路由设置,均为组内路由,可以省略这段共有路径。接着我们又调用了路由分组对象的GET方法,为获取短信验证码添加了一条路由,将GET方法结合/smscode/:mobile路径,路由到controller包的GetSMSCode函数。将GetSMSCode函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有GetSMSCode函数。
为此,我们在front-end工程目录下controller子目录中的user.go文件中定义GetSMSCode函数:
xxxxxxxxxx
111// 获取短信验证码
2func GetSMSCode(ctx *gin.Context) {
3 // 获取手机号
4 mobile := ctx.Param("mobile")
5 // 获取图片验证码字符串
6 text := ctx.Query("text")
7 // 获取图片验证码UUID
8 uuid := ctx.Query("id")
9
10 fmt.Println(mobile, text, uuid)
11}
在GetSMSCode函数里,我们直接调用了Gin框架调用此函数时传入的上下文对象的Param方法,取添加路由时放在请求路径中的占位符“mobile”为参数,接收其返回的手机号。类似地,我们还可以通过该上下文对象的Query方法,从请求URL的参数部分,获取图片验证码文本和图片ID。
为了校验用户输入的图片验证码是否正确,这里需要从Redis数据库中,以图片ID为键,读取其值,即正确的图片验证码文本。
我们在front-end工程目录下model子目录中,添加一个名为redis.go的文件,封装所有与操作Redis数据库有关的代码:
xxxxxxxxxx
181package model
2
3import "github.com/gomodule/redigo/redis"
4
5// 从Redis数据库中读取图片验证码字符串
6func ReadImageCode(uuid string) (string, error) {
7 // 连接数据库
8 conn, err := redis.Dial("tcp", "127.0.0.1:6379")
9 if err != nil {
10 return "", err
11 }
12
13 // 关闭数据库连接
14 defer conn.Close()
15
16 // 操作数据库同时类型具体化
17 return redis.String(conn.Do("get", uuid))
18}
这里我们定义了一个名为ReadImageCode的函数,用于从Redis数据库中读取图片验证码文本。该函数只带有一个输入参数,uuid即图片ID。首先调用Redigo的Dial函数,连接数据库,同时获得用于后续数据库操作的连接对象。然后通过连接对象的Do方法,以图片ID为键,读取其值,并将其类型具体化为字符串,即图片验证码文本。该函数返回图片验证码文本和访问Redis过程中可能得到的错误对象。
下面我们来编写实际发送短信验证码的代码。
回到user.go文件中的GetSMSCode函数,添加实际发送短信验证码的代码:
xxxxxxxxxx
721// 获取短信验证码
2func GetSMSCode(ctx *gin.Context) {
3 // 获取手机号
4 mobile := ctx.Param("mobile")
5 // 获取图片验证码字符串
6 text := ctx.Query("text")
7 // 获取图片验证码UUID
8 uuid := ctx.Query("id")
9
10 rsp := make(map[string]string)
11
12 // 从Redis数据库中读取图片验证码字符串
13 str, err := model.ReadImageCode(uuid)
14 if err != nil {
15 fmt.Println(err)
16 rsp["errno"] = utils.ERROR_DATABASE
17 rsp["errmsg"] = utils.StrError(rsp["errno"])
18 ctx.JSON(http.StatusOK, rsp)
19 return
20 }
21
22 // 校验图片验证码
23 if text != str {
24 fmt.Println("图片验证码校验失败")
25 rsp["errno"] = utils.ERROR_DATA
26 rsp["errmsg"] = utils.StrError(rsp["errno"])
27 ctx.JSON(http.StatusOK, rsp)
28 return
29 }
30
31 // 发送短信验证码
32 config := sdk.NewConfig()
33 credential := credentials.NewAccessKeyCredential(
34 "LTAI5tFFwAp6n28yon5iqazV",
35 "7nZ8PV1CTv7uN8VcRqbwnDG2Z1bJHw")
36 client, err := dysmsapi.NewClientWithOptions(
37 "cn-hangzhou", config, credential)
38 if err != nil {
39 fmt.Println(err)
40 rsp["errno"] = utils.ERROR_SMS
41 rsp["errmsg"] = utils.StrError(rsp["errno"])
42 ctx.JSON(http.StatusOK, rsp)
43 return
44 }
45 request := dysmsapi.CreateSendSmsRequest()
46 request.Scheme = "https"
47 request.SignName = "阿里云短信测试"
48 request.TemplateCode = "SMS_154950909"
49 request.PhoneNumbers = mobile
50 rand.Seed(time.Now().UnixNano())
51 code := fmt.Sprintf("%06d", rand.Int31n(999999))
52 request.TemplateParam = "{\"code\": \"" + code + "\"}"
53 response, err := client.SendSms(request)
54 if err != nil {
55 fmt.Println(err)
56 rsp["errno"] = utils.ERROR_SMS
57 rsp["errmsg"] = utils.StrError(rsp["errno"])
58 ctx.JSON(http.StatusOK, rsp)
59 return
60 }
61 if response.Code != "OK" {
62 fmt.Println(response.Message)
63 rsp["errno"] = utils.ERROR_SMS
64 rsp["errmsg"] = utils.StrError(rsp["errno"])
65 ctx.JSON(http.StatusOK, rsp)
66 return
67 }
68
69 rsp["errno"] = utils.ERROR_OK
70 rsp["errmsg"] = utils.StrError(rsp["errno"])
71 ctx.JSON(http.StatusOK, rsp)
72}
在从请求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文件,修改其中的代码:
xxxxxxxxxx
271package model
2
3import "github.com/gomodule/redigo/redis"
4
5// 连接池
6var pool = redis.Pool{
7 MaxIdle: 20, // 最大空闲连接数
8 MaxActive: 50, // 最大活动连接数
9 MaxConnLifetime: 5 * 60, // 最大连接生命周期
10 IdleTimeout: 60, // 空闲超时
11 Dial: func() (redis.Conn, error) { // 连接回调
12 // 连接数据库
13 return redis.Dial("tcp", "127.0.0.1:6379")
14 },
15}
16
17// 从Redis数据库中读取图片验证码字符串
18func ReadImageCode(uuid string) (string, error) {
19 // 从连接池中获取连接
20 conn := pool.Get()
21
22 // 关闭数据库连接
23 defer conn.Close()
24
25 // 操作数据库同时类型具体化
26 return redis.String(conn.Do("get", uuid))
27}
这里我们首先创建了一个Pool类型的连接池对象,并对其最大空闲连接数、最大活动连接数、最大连接生命周期、空闲超时和连接回调等属性,做必要的初始化。在后面的ReadImageCode函数中,不再显式调用Redigo的Dial函数建立与数据库的连接,而是通过连接池对象的Get方法获取连接对象。至于这个连接对象是在此刻创建的,还是先前创建恰在此时空闲的,都无所谓,连接池会把这个问题处理得很好。
定义在user.go文件中的GetSMSCode函数,现在确实已经可以向用户的手机发送短信验证码了,但根据我们在前面关于用户注册流程的描述,该函数还应该可以将用户的手机号和短信验证码,以键值对的形式保存到Redis数据库中。
为此,我们首先在前端model目录下的redis.go文件中,添加一个名为SaveSMSCode的函数:
xxxxxxxxxx
121// 将手机号和短信验证码保存到Redis数据库中
2func SaveSMSCode(mobile, code string) error {
3 // 从连接池中获取连接
4 conn := pool.Get()
5
6 // 关闭数据库连接
7 defer conn.Close()
8
9 // 操作数据库
10 _, err := conn.Do("setex", mobile, 60*3, code)
11 return err
12}
在这段代码中,我们首先从Redis连接池中获取连接对象,然后通过该对象的Do方法,将从参数传入的用户手机号和短信验证码,以键值对的形式保存到Redis数据库中,并返回该方法返回的错误对象。
然后,我们还需要修改前端controller目录下的user.go文件,在GetSMSCode函数中添加对model包SaveSMSCode函数的调用:
xxxxxxxxxx
841...
2// 获取短信验证码
3func GetSMSCode(ctx *gin.Context) {
4 // 获取手机号
5 mobile := ctx.Param("mobile")
6 // 获取图片验证码字符串
7 text := ctx.Query("text")
8 // 获取图片验证码UUID
9 uuid := ctx.Query("id")
10
11 rsp := make(map[string]string)
12
13 // 从Redis数据库中读取图片验证码字符串
14 str, err := model.ReadImageCode(uuid)
15 if err != nil {
16 fmt.Println(err)
17 rsp["errno"] = utils.ERROR_DATABASE
18 rsp["errmsg"] = utils.StrError(rsp["errno"])
19 ctx.JSON(http.StatusOK, rsp)
20 return
21 }
22
23 // 校验图片验证码
24 if text != str {
25 fmt.Println("图片验证码校验失败")
26 rsp["errno"] = utils.ERROR_DATA
27 rsp["errmsg"] = utils.StrError(rsp["errno"])
28 ctx.JSON(http.StatusOK, rsp)
29 return
30 }
31
32 // 发送短信验证码
33 config := sdk.NewConfig()
34 credential := credentials.NewAccessKeyCredential(
35 "LTAI5tFFwAp6n28yon5iqazV",
36 "7nZ8PV1CTv7uN8VcRqbwnDG2Z1bJHw")
37 client, err := dysmsapi.NewClientWithOptions(
38 "cn-hangzhou", config, credential)
39 if err != nil {
40 fmt.Println(err)
41 rsp["errno"] = utils.ERROR_SMS
42 rsp["errmsg"] = utils.StrError(rsp["errno"])
43 ctx.JSON(http.StatusOK, rsp)
44 return
45 }
46 request := dysmsapi.CreateSendSmsRequest()
47 request.Scheme = "https"
48 request.SignName = "阿里云短信测试"
49 request.TemplateCode = "SMS_154950909"
50 request.PhoneNumbers = mobile
51 rand.Seed(time.Now().UnixNano())
52 code := fmt.Sprintf("%06d", rand.Int31n(999999))
53 request.TemplateParam = "{\"code\": \"" + code + "\"}"
54 response, err := client.SendSms(request)
55 if err != nil {
56 fmt.Println(err)
57 rsp["errno"] = utils.ERROR_SMS
58 rsp["errmsg"] = utils.StrError(rsp["errno"])
59 ctx.JSON(http.StatusOK, rsp)
60 return
61 }
62 if response.Code != "OK" {
63 fmt.Println(response.Message)
64 rsp["errno"] = utils.ERROR_SMS
65 rsp["errmsg"] = utils.StrError(rsp["errno"])
66 ctx.JSON(http.StatusOK, rsp)
67 return
68 }
69
70 // 将手机号和短信验证码保存到Redis数据库中
71 err = model.SaveSMSCode(mobile, code)
72 if err != nil {
73 fmt.Println(err)
74 rsp["errno"] = utils.ERROR_DATABASE
75 rsp["errmsg"] = utils.StrError(rsp["errno"])
76 ctx.JSON(http.StatusOK, rsp)
77 return
78 }
79
80 rsp["errno"] = utils.ERROR_OK
81 rsp["errmsg"] = utils.StrError(rsp["errno"])
82 ctx.JSON(http.StatusOK, rsp)
83}
84...
在完成短信验证码的发送之后,调用model包的SaveSMSCode函数,将用户的手机号和短信验证码,保存到Redis数据库中。
重启前端服务器。在注册页面填入手机号和图片验证码,点击“获取验证码”。在手机收到短信验证码的同时,Redis数据库中记录了手机号——短信验证码键值对。
我们在前端服务器中,成功地实现了获取短信验证码的业务逻辑。这令我们备受鼓舞,但作为一个前后端分离的项目,这种纯业务性的功能,放在前端服务器中是不合适的。因此我们有必要将与获取短信验证码有关的操作,移植到后端服务器中,且用一个独立的微服务予以实现。
首先,我们来创建一个这样的微服务。
我们可以直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,所创建的微服务名为GetSMSCode。
接着,需要修改一下go-micro为我们自动生成的接口描述。
在back-end工程目录下的GetSMSCode微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为GetSMSCode.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:
xxxxxxxxxx
121...
2message CallRequest {
3 string mobile = 1;
4 string text = 2;
5 string uuid = 3;
6}
7
8message CallResponse {
9 string errno = 1;
10 string errmsg = 2;
11}
12...
这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,其中包含三个字符串,分别表示用户的手机号、用户输入的图片验证码文本和图片ID。CallResponse表示后端返回给前端的响应数据,其中包含两个字符串,错误代码和错误描述。
有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。
为此,我们在GetSMSCode微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。
前面我们曾经讲过,go-micro自动生成的微服务代码框架,默认使用的服务发现是MDNS,而如果我们想改用Consul,就需要修改部分代码。与此同时,我们还需要为微服务进程,设置绑定的IP地址和端口号。
为此,我们需要修改一下GetSMSCode微服务目录中的main.go文件:
xxxxxxxxxx
121...
2func main() {
3 ...
4 srv.Init(
5 micro.Name(service),
6 micro.Version(version),
7 micro.Registry(consul.NewRegistry()),
8 micro.Address("127.0.0.1:9002"),
9 )
10 ...
11}
12...
我们在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9002端口。
我们已经在前端服务器的模型层,编写了与Redis数据库访问有关的代码。现在需要将其移植到后端微服务中。
我们在GetSMSCode微服务目录下,创建一个名为model的子目录,表示该微服务的模型层。在该子目录中创建redis.go文件,封装所有与操作Redis数据库有关的代码:
xxxxxxxxxx
401package model
2
3import "github.com/gomodule/redigo/redis"
4
5// 连接池
6var pool = redis.Pool{
7 MaxIdle: 20, // 最大空闲连接数
8 MaxActive: 50, // 最大活动连接数
9 MaxConnLifetime: 5 * 60, // 最大连接生命周期
10 IdleTimeout: 60, // 空闲超时
11 Dial: func() (redis.Conn, error) { // 连接回调
12 // 连接数据库
13 return redis.Dial("tcp", "127.0.0.1:6379")
14 },
15}
16
17// 从Redis数据库中读取图片验证码字符串
18func ReadImageCode(uuid string) (string, error) {
19 // 从连接池中获取连接
20 conn := pool.Get()
21
22 // 关闭数据库连接
23 defer conn.Close()
24
25 // 操作数据库同时类型具体化
26 return redis.String(conn.Do("get", uuid))
27}
28
29// 将手机号和短信验证码保存到Redis数据库中
30func SaveSMSCode(mobile, code string) error {
31 // 从连接池中获取连接
32 conn := pool.Get()
33
34 // 关闭数据库连接
35 defer conn.Close()
36
37 // 操作数据库
38 _, err := conn.Do("setex", mobile, 60*3, code)
39 return err
40}
这段代码和我们之前为前端服务器编写的代码几乎完全一样。在全局域定义一个Redis连接池对象。ReadImageCode函数用于根据图片ID,从Redis数据库中读取图片验证码文本。SaveSMSCode函数用于将用户的手机号和短信验证码,以键值对的形式保存到Redis数据库中。
后端微服务有时会用到和前端服务器一样的实用工具,比如有关错误代码和错误描述的定义,或者类似StrError这种,将错误代码转换为错误描述的工具函数。
我们可以直接将front-end工程目录下的utils子目录,原封不动地复制到back-end工程目录下的GetSMSCode微服务目录中。
下一步,我们要进入微服务的关键部分,即实现业务逻辑。
打开GetSMSCode微服务目录下,handler子目录中的GetSMSCode.go文件,在其中的Call方法里,添加与获取短信验证码有关的操作:
xxxxxxxxxx
711...
2// 获取短信验证码
3func (e *GetSMSCode) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received GetSMSCode.Call request: %v", req)
5
6 // 从Redis数据库中读取图片验证码字符串
7 str, err := model.ReadImageCode(req.Uuid)
8 if err != nil {
9 logger.Error(err)
10 rsp.Errno = utils.ERROR_DATABASE
11 rsp.Errmsg = utils.StrError(rsp.Errno)
12 return nil
13 }
14
15 // 校验图片验证码
16 if req.Text != str {
17 logger.Error("图片验证码校验失败")
18 rsp.Errno = utils.ERROR_DATA
19 rsp.Errmsg = utils.StrError(rsp.Errno)
20 return nil
21 }
22
23 // 发送短信验证码
24 config := sdk.NewConfig()
25 credential := credentials.NewAccessKeyCredential(
26 "LTAI5tFFwAp6n28yon5iqazV",
27 "7nZ8PV1CTv7uN8VcRqbwnDG2Z1bJHw")
28 client, err := dysmsapi.NewClientWithOptions(
29 "cn-hangzhou", config, credential)
30 if err != nil {
31 logger.Error(err)
32 rsp.Errno = utils.ERROR_SMS
33 rsp.Errmsg = utils.StrError(rsp.Errno)
34 return nil
35 }
36 request := dysmsapi.CreateSendSmsRequest()
37 request.Scheme = "https"
38 request.SignName = "阿里云短信测试"
39 request.TemplateCode = "SMS_154950909"
40 request.PhoneNumbers = req.Mobile
41 rand.Seed(time.Now().UnixNano())
42 code := fmt.Sprintf("%06d", rand.Int31n(999999))
43 request.TemplateParam = "{\"code\": \"" + code + "\"}"
44 response, err := client.SendSms(request)
45 if err != nil {
46 logger.Error(err)
47 rsp.Errno = utils.ERROR_SMS
48 rsp.Errmsg = utils.StrError(rsp.Errno)
49 return nil
50 }
51 if response.Code != "OK" {
52 logger.Error(response.Message)
53 rsp.Errno = utils.ERROR_SMS
54 rsp.Errmsg = utils.StrError(rsp.Errno)
55 return nil
56 }
57
58 // 将手机号和短信验证码保存到Redis数据库中
59 err = model.SaveSMSCode(req.Mobile, code)
60 if err != nil {
61 logger.Error(err)
62 rsp.Errno = utils.ERROR_DATABASE
63 rsp.Errmsg = utils.StrError(rsp.Errno)
64 return nil
65 }
66
67 rsp.Errno = utils.ERROR_OK
68 rsp.Errmsg = utils.StrError(rsp.Errno)
69 return nil
70}
71...
这段代码与它的前端版本大体一致。只是在这里,用户的手机号、用户输入的图片验证码文本和图片ID,均来自前端服务器传送过来的CallRequest请求对象,而非浏览器页面提交的请求URL。同时将错误代码、错误描述等需要回传给前端服务器的响应数据,放到CallResponse响应对象中。
微服务形式的后端服务器已然就绪,我们可以直接启动它。
启动GetSMSCode微服务,用浏览器访问localhost:8500。我们可以看到GetSMSCode微服务处于健康状态。
最后一步,我们还需要在前端服务器的路由处理函数中,调用后端微服务中的远程方法。
前端和后端共用同一套接口描述。因此,这里我们将GetSMSCode微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetSMSCode目录中。
打开front-end工程目录下controller子目录中的user.go文件。在该文件的import部分增加一行:
xxxxxxxxxx
71...
2import (
3 ...
4 pbGetSMSCode "iHome/front-end/proto/GetSMSCode"
5 ...
6)
7...
修改其中的路由处理函数GetSMSCode,将之前发送短信验证码的代码,替换为调用微服务远程方法的代码:
xxxxxxxxxx
271...
2// 获取短信验证码
3func GetSMSCode(ctx *gin.Context) {
4 // 获取手机号
5 mobile := ctx.Param("mobile")
6 // 获取图片验证码字符串
7 text := ctx.Query("text")
8 // 获取图片验证码UUID
9 uuid := ctx.Query("id")
10
11 // 调用微服务
12 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
13 srv.Init()
14 c := pbGetSMSCode.NewGetSMSCodeService("getsmscode", srv.Client())
15 rsp, err := c.Call(context.Background(), &pbGetSMSCode.CallRequest{
16 Mobile: mobile,
17 Text: text,
18 Uuid: uuid,
19 })
20 if err != nil {
21 fmt.Println(err)
22 return
23 }
24
25 ctx.JSON(http.StatusOK, rsp)
26}
27...
这段代码通过Consul服务发现,获取了有关名为getsmscode的微服务的信息,远程调用了其中的GetSMSCode对象的Call方法。该方法以包含用户手机号、图片验证码文本和图片ID等信息的请求对象为参数,并返回包含错误代码和错误描述的响应对象。最后,将该响应对象序列化为一个JSON字符串,编码到HTTP响应中,回传给浏览器。
重启前端服务器。在注册页面填入手机号和图片验证码,点击“获取验证码”。在手机收到短信验证码的同时,Redis数据库中记录了手机号——短信验证码键值对。
谢谢大家,我们下节课再见!