同学们好!我们的《我家租房网》应用截至目前已经可以打开默认首页了,但在进入注册页面时,我们会发现获取图片验证码失败。这节课我们将着手解决这个问题。
为了解决这个问题,我们将分步骤地为大家讲解。首先我们要设法从来自浏览器的请求路径中获取图片ID,然后为获取图片验证码添加路由,接着我们会先利用一个第三方工具尝试生成图片验证码,再将此功能集成到前端服务器的路由处理函数中,最后我们会把有关生成图片验证码的操作实现为一个独立的后端微服务,并被前端服务器远程调用。
在解决有关图片验证码的问题之前,我们需要先把用户注册的业务流程搞清楚。在这个项目中,我们采用了基于图片验证码和短信验证码的双重验证机制。这一点在注册页面中可以直观地看到。当注册页面打开时,浏览器会向服务器发送一个图片ID,服务器响应浏览器一幅图片。图片中包含一串由字母和数字组成的文本,这就是图片验证码。同时服务器会将图片ID和图片验证码文本,以键值对的形式保存起来。用户将在图片中看到的文本,输入到注册表单的“图片验证码”编辑框中。当用户在表单中填好手机号,并按下“获取短信验证码”按钮时,浏览器会将图片ID、用户输入的图片验证码文本和手机号一起发送给服务器。服务器以收到的图片ID为键,从存储中查到正确的图片验证码文本,验证用户输入的图片验证码文本是否正确。如果正确,则随机生成一个短信验证码,根据用户的手机号,通过短信平台发送到用户的手机上,同时将手机号和短信验证码,以键值对的形式保存起来。用户将通过手机收到的短信验证码,输入到注册表单的“短信验证码”编辑框中。当用户在表单中设置好密码后,点击“立即注册”按钮。这时浏览器会将手机号、用户输入的短信验证码和密码一起发送给服务器。服务器以收到的手机号为键,从存储中查到正确的短信验证码,验证用户输入的短信验证码是否正确。如果正确,则将用户的手机号和密码哈希值保存到数据库中,完成注册。整个注册流程的第一步,就是从来自浏览器的请求中获取图片ID。
启动前端,用浏览器访问localhost:8080/home,点击“注册”按钮,进入注册页面。这时在调试窗格中会看到一条错误信息。信息的形式是一串被四个减号分隔为五组的十六进制数。点击这条信息并选择Headers标签,我们会看到获取图片验证码的请求URL,其中就包含我们刚刚提到的那串被四个减号分隔为五组的十六进制数,它就是图片ID。图片ID是由运行在浏览器中的JavaScript代码动态生成的,每次请求都不一样。这样的动态信息是不能在路由中写死的,而要使用特殊的占位符,如“:uuid”,占位,并通过程序代码在运行时从请求中动态获取。
那么如何获取请求路径中的参数呢?
为了获取请求路径中的参数,我们可以在路由处理函数中,以占位符字符串,如“uuid”,注意这里没有冒号,作为参数,调用Gin框架调用该函数时传入的上下文对象的Param方法,其返回值即为与该占位符相对应的请求参数。
下面我们将为前端服务器添加有关获取图片验证码的路由处理。
这里我们要为获取图片验证码添加一条路由,GET方法结合/api/v1.0/imagecode/:uuid路径,处理函数名为GetImageCode。
在front-end工程目录下的main.go文件中添加一条路由:
1...
2func main() {
3 ...
4 // 路由匹配
5 ...
6 router.GET("/api/v1.0/imagecode/:uuid", controller.GetImageCode)
7 ...
8}
9...
这里我们调用了路由对象的GET方法,为获取图片验证码添加了一条路由,将GET方法结合/api/v1.0/imagecode/:uuid路径,路由到controller包的GetImageCode函数。将GetImageCode函数定义在controller包里是因为该函数的主要任务是执行业务逻辑,属于MVC中的C,即控制器层的部分。
当然,在controller包里真的得有GetImageCode函数。
为此,我们在front-end工程目录下controller子目录中的user.go文件中定义GetImageCode函数:
xxxxxxxxxx
81...
2// 获取图片验证码
3func GetImageCode(ctx *gin.Context) {
4 // 获取图片验证码的UUID
5 uuid := ctx.Param("uuid")
6 fmt.Println(uuid)
7}
8...
在GetImageCode函数里,我们直接调用了Gin框架调用此函数时传入的上下文对象的Param方法,取添加路由时放在请求路径中的占位符“uuid”为参数,接收其返回的图片ID并打印。
启动前端,用浏览器访问localhost:8080/home,点击“注册”按钮,进入注册页面。调试窗格中的错误信息消失。同时前端服务器会在控制台上输出那个被四个减号分隔为五组的十六进制数。这就是我们从请求路径中获取并打印出来的图片ID。
按照前面介绍的注册流程,在服务器收到图片ID以后,应该生成一幅包含一组字母和数字的图片,作为图片验证码,响应给浏览器。那么如何生成图片验证码呢?
这里我们将借助一个第三方库来生成图片验证码,首先要安装这个库。
能够生成图片验证码的开源三方库其实有很多,我们可以在GitHub上选择一个,下载并安装到正确的位置。
相应的依赖当然也不可或缺。
在正式将图片验证码生成器应用于项目之前,我们不妨先编写一个测试程序,体验一下这个第三方库的使用效果。
我们在front-end工程目录下的test子目录中创建一个名为captcha.go的文件:
x1package main
2
3import (
4 "github.com/afocus/captcha"
5 "image/color"
6 "image/png"
7 "net/http"
8)
9
10func main() {
11 http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
12 cap := captcha.New() // 创建图片验证码生成器
13 cap.SetFont("../resources/comic.ttf") // 设置字体
14 cap.SetSize(128, 64) // 设置图片大小
15 cap.SetDisturbance(captcha.MEDIUM) // 设置干扰强度
16 cap.SetFrontColor(color.RGBA{255, 255, 255, 255}) // 设置前景色
17 cap.SetBkgColor(
18 color.RGBA{255, 0, 0, 255},
19 color.RGBA{0, 255, 0, 255},
20 color.RGBA{0, 0, 255, 255}) // 设置背景色
21 img, str := cap.Create(4, captcha.ALL) // 生成图片验证码
22
23 png.Encode(w, img)
24
25 println(str)
26 })
27
28 http.ListenAndServe(":8888", nil)
29}
在这个测试程序中,我们没有使用Gin框架,而是直接通过Go语言自带的http包,构建了一个简单的Web服务器。针对请求路径“/”的路由处理函数是一个匿名函数。在该处理函数中,首先通过captcha包的New方法,创建了一个图片验证码生成器对象。而后通过该对象的一系列方法,设置了图片验证码的字体、图片大小、干扰强度、前景色和背景色等各种属性。其中背景色可以设置多个,实际使用的背景色将从中随机选择一个。前景色和背景色均以RGBA的形式给出,四个颜色分量分别表示红、绿、蓝和不透明的程度,为0到255之间的整数,越小越弱,越大越强。一切准备就绪后,通过图片验证码生成器对象的Create方法,获得验证码图片和文本。该方法带有两个参数,第一个参数表示验证码的字符个数,第二个参数表示验证码的字符类型,纯字母,纯数字,还是字母数字都有。最后,通过png包的Encode方法,将验证码图片按PNG格式,编码到HTTP响应中,回传给浏览器,同时将验证码文本打印到控制台。在main函数的最后,我们调用了http包的ListenAndServe函数,启动针对本机任意地址8888端口的监听,并等待连接。
这里我们为图片验证码设置的字体文件comic.ttf,位于front-end工程目录的resources子目录下。直接在test目录下通过“go run”命令启动测试程序。
用浏览器访问localhost:8888,可以看到验证码图片显示在浏览器窗口中,验证码文本显示在测试程序的控制台上,二者应该是完全一致的。
下一步,我们将把测试程序中用于生成图片验证码的代码,集成到前端服务器的路由处理函数中。
回到我们先前编写的GetImageCode函数,增加用于生成图片验证码的代码:
xxxxxxxxxx
201...
2// 获取图片验证码
3func GetImageCode(ctx *gin.Context) {
4 // 获取图片验证码的UUID
5 uuid := ctx.Param("uuid")
6 fmt.Println(uuid)
7
8 cap := captcha.New()
9 cap.SetFont("./resources/comic.ttf")
10 cap.SetSize(128, 64)
11 cap.SetDisturbance(captcha.MEDIUM)
12 cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
13 cap.SetBkgColor(color.RGBA{49, 174, 54, 255})
14 img, str := cap.Create(4, captcha.NUM)
15
16 png.Encode(ctx.Writer, img)
17
18 println(str)
19}
20...
这段代码与之前的测试程序相比只有几处不太一样。一个是字体文件的相对路径,一个是图片验证码的背景颜色,再一个是图片验证码的字符类型。另外,由于这里使用了Gin框架,传给png包Encode方法的第一个参数是一个响应输出器对象,取自从框架传入的上下文参数。
重启前端,用浏览器访问localhost:8080/home,点击“注册”按钮,进入注册页面。可以看到图片验证码已经可以正常显示。
同时,在前端服务器的控制台上,图片验证码文本被打印在日志中。
如果我们所构建的是一个传统的,非前后端分离的项目,做到现在这一步就已经非常完美了。但作为一个前后端分离的项目,把与获取图片验证码有关的操作,放在前端服务器中是不合适的,因为这部分功能是属于纯业务的,即使我们的客户机不是一个Web客户机,也应该可以使用这项功能。因此我们有必要将与获取图片验证码有关的操作,移植到后端服务器中。而且因为我们采用了微服务架构,所以这项功能完全可以用一个独立的微服务来实现。
首先,我们来创建一个这样的微服务。
我们可以直接在back-end工程目录下执行go-micro命令,子命令为new,参数为service,所创建的微服务名为GetImageCode。
接着,需要修改一下go-micro为我们自动生成的接口描述。
在back-end工程目录下的GetImageCode微服务目录中,有一个名为proto的子目录,这就是存放接口描述脚本及其Go代码文件的地方。现阶段里面只有一个名为GetImageCode.proto的ProtoBuf脚本文件。我们需要对该文件做一些修改:
xxxxxxxxxx
91...
2message CallRequest {
3}
4
5message CallResponse {
6 // 用字节切片存储验证码图片的JSON字符串
7 bytes img = 1;
8}
9...
这段代码描述的是,前端服务器和后端微服务之间的数据交换格式。CallRequest表示前端提供给后端的请求数据,这里为空。CallResponse表示后端返回给前端的响应数据,其中只有一个字节流形式的JSON字符串,该字符串来自对验证码图片的序列化。
有了用ProtoBuf语言描述的远程调用接口,我们还需要把它编译成基于Go语言的程序代码。
为此,我们在GetImageCode微服务目录中执行make命令,并携带四个目标参数,它们是init、proto、update和tidy。
前面我们曾经讲过,go-micro自动生成的微服务代码框架,默认使用的服务发现是MDNS,而如果我们想改用Consul,就需要修改部分代码。与此同时,我们还需要为微服务进程,设置绑定的IP地址和端口号。
为此,我们需要修改一下GetImageCode微服务目录中的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:9001"),
9 )
10 ...
11}
12...
我们在服务器对象的初始化部分,指示其注册到Consul服务器,同时绑定本机的9001端口。
下一步,我们要进入微服务的关键部分,即实现业务逻辑。
打开GetImageCode微服务目录下,handler子目录中的GetImageCode.go文件,在其中的Call方法里,添加与获取图片验证码有关的操作:
xxxxxxxxxx
271...
2// 获取图片验证码
3func (e *GetImageCode) Call(ctx context.Context, req *pb.CallRequest, rsp *pb.CallResponse) error {
4 logger.Infof("Received GetImageCode.Call request: %v", req)
5
6 // 生成图片验证码
7 cap := captcha.New()
8 cap.SetFont("./resources/comic.ttf")
9 cap.SetSize(128, 64)
10 cap.SetDisturbance(captcha.MEDIUM)
11 cap.SetFrontColor(color.RGBA{255, 255, 255, 255})
12 cap.SetBkgColor(color.RGBA{49, 174, 54, 255})
13 img, str := cap.Create(4, captcha.NUM)
14
15 // 序列化验证码图片
16 jsonImg, err := json.Marshal(img)
17 if err != nil {
18 logger.Error(err)
19 return err
20 }
21 rsp.Img = jsonImg
22
23 fmt.Println(str)
24
25 return nil
26}
27...
这段代码与它的前端版本大体一致。只是在这里,我们将从图片验证码生成器对象获得的验证码图片,序列化为一个字节切片类型的JSON字符串,放到返回给前端的响应数据中。
微服务形式的后端服务器已然就绪,我们可以直接启动它。
注意,生成图片验证码的代码现在已经位于后端服务器中,我们需要把图片验证码字体文件comic.ttf,放到GetImageCode微服务目录的resources子目录下。现在,我们可以启动Consul服务器。
然后启动GetImageCode微服务,用浏览器访问localhost:8500。我们可以看到GetImageCode微服务处于健康状态。
最后一步,我们还需要在前端服务器的路由处理函数中,调用后端微服务中的远程方法。
为了在前端服务器代码中访问go-micro框架提供的客户机接口,我们还需要安装go-micro库及其所依赖的一系列软件包。
前端和后端共用同一套接口描述。因此,这里我们将GetImageCode微服务目录下proto子目录中的所有文件,原封不动地复制到front-end工程目录proto子目录下的GetImageCode目录中。
打开front-end工程目录下controller子目录中的user.go文件。在该文件的import部分增加一行:
xxxxxxxxxx
71...
2import (
3 ...
4 pbGetImageCode "iHome/front-end/proto/GetImageCode"
5 ...
6)
7...
修改其中的路由处理函数GetImageCode,将之前生成图片验证码的代码,替换为调用微服务远程方法的代码:
xxxxxxxxxx
231...
2// 获取图片验证码
3func GetImageCode(ctx *gin.Context) {
4 // 获取图片验证码的UUID
5 uuid := ctx.Param("uuid")
6 fmt.Println(uuid)
7
8 // 调用微服务
9 srv := micro.NewService(micro.Registry(consul.NewRegistry()))
10 srv.Init()
11 c := pbGetImageCode.NewGetImageCodeService("getimagecode", srv.Client())
12 rsp, err := c.Call(context.Background(), &pbGetImageCode.CallRequest{})
13 if err != nil {
14 fmt.Println(err)
15 return
16 }
17
18 // 反序列化验证码图片
19 var img captcha.Image
20 json.Unmarshal(rsp.Img, &img)
21 png.Encode(ctx.Writer, img)
22}
23...
这段代码通过Consul服务发现,获取了有关名为getimagecode的微服务的信息,远程调用了其中的GetImageCode对象的Call方法。该方法返回的响应中包含一个字节切片类型的JSON字符串,将其反序列化即得到验证码图片。最后,通过png包的Encode方法,将验证码图片按PNG格式,编码到HTTP响应中,回传给浏览器。
由于先前已经启动了Consul服务器和GetImageCode微服务,现在只需启动前端服务器。从浏览器进入注册页面,可以看到图片验证码已经可以正常显示。
同时,分别在前端服务器和GetImageCode微服务的控制台中,输出了图片ID和验证码文本。
谢谢大家,我们下节课再见!