思考并回答以下问题:
实验介绍
介绍完了Gin框架中的控制器和路由,现在将着重讲解另一个重要的模块中间件(Middleware)。本次实验将通过介绍请求-响应生命周期以及中间件的注册和自定义,让学员对Gin框架中间件相关的知识加深印象。
知识点
- HTTP请求生命周期
- 注册中间件
- 自定义中间件
HTTP请求生命周期
一个完整流程
谈到Web框架中间件,我们不得不提及一下一个HTTP请求-响应过程的生命周期。之前的实验中,我们知道Gin框架中的控制器会通过上下文接收HTTP请求,然后可以在处理函数中解析请求参数,最后通过一系列操作后返回HTTP响应给客户端。
这很直观,但过于简化。在大型Web应用中,一个HTTP请求生命周期远不止控制器处理函数中的逻辑。下面我们来看看一个典型的HTTP请求所经历的过程。
HTTP请求的大致流程如下:
1,客户端(或者浏览器)发起一个HTTP请求;
2,HTTP请求首先进入到中间件;
3,中间件处理完后,转交给控制器处理函数;
4,处理函数处理完毕后,返回HTTP响应给中间件;
5,中间件处理完后,返回最终HTTP响应给客户端。
因此,当我们编写控制器逻辑时,需要知道的是,上下文中的HTTP请求已经由中间件处理过了,而返回的响应也会经由中间件处理后再下发给客户端。这是一个链式流程(Chaining Process)。
使用中间件的原因
为什么要在客户端和控制器中间加上这么多中间件呢?这是因为,很多经过不同路由的HTTP请求,都会需要相同的处理逻辑。例如,对于每一个需要权限验证的HTTP请求,我们都需要进行校验,而这个校验逻辑又是一样的。因此,为了避免重复,我们将这部分通用的处理逻辑抽象成公共模块,也就是中间件。
注册中间件
在Gin框架中,中间件跟控制器、路由一样,是需要注册到Gin引擎实例中的,关键方法是app.Use
。
我们看一下app.Use
的定义,代码如下。1
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes
看到了么?注册中间件的方法Use的参数是HandlerFunc类型,也就是处理函数,也就意味着中间件函数的形式跟控制器处理函数一样,都是func(c *Context)
这个函数作为参数来注册。因此,在中间件中,主要操作对象还是上下文(Context)。
下面我们用实战练习来体会如何注册中间件。
在项目根目录创建一个文件夹middlewares,在其中创建文件info.go,表示获取HTTP请求信息的中间件。在info.go中输入以下内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package middlewares
import (
"fmt"
"github.com/gin-gonic/gin"
)
func InfoMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
fmt.Println(fmt.Sprintf("requestUri: %s", c.Request.RequestURI)) // 请求 URI
fmt.Println(fmt.Sprintf("requestMethod: %s", c.Request.Method)) // 请求方法
fmt.Println(fmt.Sprintf("remoteIp: %s", c.RemoteIP())) // 远程 IP 地址
// 进行下一步(中间件或控制器)
c.Next()
}
}
然后,打开main.go,在注册控制器代码前,加入如下代码,注册刚才定义的中间件。1
2// 注册中间件
app.Use(middlewares.InfoMiddleware())
打开终端,执行go run main.go
启动应用。
成功启动后,访问默认URL。成功获取响应后,你应该可以看到下面类似的输出。
可以注意到,HTTP请求的URI、方法、IP都获取到并输出在终端上了。
当然,你也可以试一下其他的URL,看看会得到什么。
自定义中间件
我们刚刚在上一节注册中间件中,已经对自定义中间件做了示例,但那个相对来说比较简单,这一小节我们讲介绍如何自己编写一个有用的自定义中间件。
你可能会注意到,我们在InfoMiddleware中调用了上下文中的下一步操作方法c.Next()
。这在中间件中是必须的,表示该中间件的处理逻辑已经完成,可以将HTTP请求或响应交给下一个环节了,即交由下一个中间件或控制器来处理,或者将响应下发给客户端。如果你还对本实验的第一节HTTP请求生命周期中的示意图有印象的话,应该会比较容易理解。
不过,我们具体要在中间件里做什么呢?还记得之前讲解Gin框架控制器的实验么,那里介绍了上下文的一些方法,而它们中的很多都可以直接在中间件中使用。例如,如果我们想实现一个通用的获取用户信息的中间件,就可以先调用c.Cookie
、c.GetHeader
来获取令牌中包含的用户ID,再根据ID在数据库中获取用户,并调用c.Set
保存在上下文中,以供下一个环节使用(通过调用c.Get
获取)。
现在,我们打算实现一个简单且真正有用的自定义中间件,即保存ID信息的中间件。
先理清一下思路,我们首先通过上下文获取并保存动态路由中的ID信息。然后,如果为授权用户,我们在上下文中设置为验证成功;如果没有,则设置为验证失败。当然,真实项目肯定比这个更复杂,作为练习我们先这样简化。
创建middlewares/id.go,输入以下内容。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package middlewares
import "github.com/gin-gonic/gin"
func IdMiddleware() func(c *gin.Context) {
return func(c *gin.Context) {
// ID
id := c.Param("id")
// 验证状态
auth := id == "zhangsan"
// 保存验证状态
c.Set("auth", auth)
// 如果验证成功,保存 ID
c.Set("id", id)
// 下一步
c.Next()
}
}
然后,我们打开routes/users.go,定位到获取用户函数getUser,做一点小小的调整,将id := c.Param("id")
更改为c.GetString("id")
,去掉校验逻辑,并将验证状态作为响应数据返回。修改后代码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 获取用户
func getUser(c *gin.Context) {
// 用户 ID
id := c.GetString("id")
// 用户
user := map[string]interface{}{
"username": id,
"role": "normal",
"auth": c.GetBool("auth"),
}
// 返回响应
c.AbortWithStatusJSON(http.StatusOK, user)
}
最后,我们需要新注册新创建的自定义中间件。打开main.go,在注册控制器的地方前面,添加如下代码。1
app.Use(middlewares.IdMiddleware())
编辑完成后,我们在终端中执行go run main.go
启动应用。
成功启动后,更改URL相对路径为/users/zhangsan,结果如下。
可以看到,验证状态为true,用户名也成功输出,表示ID和验证状态信息都在中间件中处理及保存成功了!
当然,我们也可以验证一下其他用户的情况。更改URL相对路径为/users/lisi,结果如下。
1 | { |
验证状态为失败,用户名成功返回,这也是符合预期的。
咱们在介绍Gin框架的时候有提及,其实Gin框架有非常好的生态,也就是说有很多开发者在GitHub上贡献了Gin框架的扩展功能,其中就包含Gin框架中间件。由于Gin框架自定义中间件编写起来非常简单,因此也存在很多官方的、非官方的中间件。
Gin框架开发团队也很贴心的创建了GitHub仓库gin-gonic/contrib,其中就包含大量的非常实用的中间件。如果你对Gin框架常用中间件感兴趣,可以到gin-gonic/contrib这个仓库中进行浏览,还可以进到对应的GitHub仓库源码中学习,看看其他程序员是如何开发自定义中间件的,这样也一定会让你对Gin框架中间件理解更深刻。
实验总结
本次实验通过学习Gin框架中HTTP请求生命周期、如何注册中间件、以及编写自定义中间件,让学员能够熟悉掌握Gin框架中间件相关的知识和应用。这也是为后面学习Gin框架鉴权、权限控制等高级功能打基础。前面这三个实验主要都在讲Gin框架或Web框架基础,接下来我们将讲解稍微偏实用的功能,也就是权限控制。