中间件middleware
Quote
golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。
我们之前说过,Context
是 Gin
的核心, 它的构造如下:
type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
Params Params
handlers HandlersChain
index int8
fullPath string
engine *Engine
params *Params
skippedNodes *[]skippedNode
// 这个互斥对象保护密钥映射。
mu sync.RWMutex
// Keys是专门用于每个请求上下文的键/值对。
Keys map[string]any
// Errors是附加到使用此上下文的所有处理程序/中间件的错误列表。
Errors errorMsgs
// Accepted定义用于内容协商的手动接受格式的列表。
Accepted []string
// queryCache缓存c.Request.URL.query()中的查询结果.
queryCache url.Values
// formCache缓存c.Request.PostForm,它包含来自POST、PATCH、或PUT主体参数。
formCache url.Values
// SameSite允许服务器定义cookie属性,这使得浏览器将此cookie与跨站点请求一起发送。
sameSite http.SameSite
}
handlers
我们通过源码可以知道就是[]HandlerFunc
. 而它的签名正是:所以中间件和我们普通的
HandlerFunc
没有任何区别, 我们怎么写HandlerFunc
就可以怎么写一个中间件.
全局中间件
-
先定义一个中间件函数:
该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。func MiddleWare() gin.HandlerFunc { return func(c *gin.Context) { t := time.Now() fmt.Println("before middleware") //设置request变量到Context的Key中,通过Get等函数可以取得 c.Set("request", "client_request") //发送request之前 c.Next() //发送requst之后 // 这个c.Write是ResponseWriter,我们可以获得状态等信息 status := c.Writer.Status() fmt.Println("after middleware,", status) t2 := time.Since(t) fmt.Println("time:", t2) } }
使用router装饰中间件,然后在/middlerware即可读取request的值,注意在router.Use(MiddleWare())代码以上的路由函数,将不会有被中间件装饰的效果。router := gin.Default() router.Use(MiddleWare()) { router.GET("/middleware", func(c *gin.Context) { //获取gin上下文中的变量 request := c.MustGet("request").(string) req, _ := c.Get("request") fmt.Println("request:",request) c.JSON(http.StatusOK, gin.H{ "middile_request": request, "request": req, }) }) } router.Run(":80")
Info
使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。
运行项目,可以在终端输入命令进行访问
完整代码
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
"time"
)
func main() {
r := gin.Default()
// 中间件绑定路由
r.Use(MiddleWare())
{
r.GET("/middleware", func(context *gin.Context) {
// 获取gin上下文中的变量
request := context.MustGet("request").(string)
req, _ := context.Get("request")
fmt.Println("request:", request)
context.JSON(http.StatusOK, gin.H{
"middle_ware_req": request,
"request": req,
})
})
}
r.Run(":80")
}
// 定义路由,重写HandlerFunc方法实现中间件
func MiddleWare() gin.HandlerFunc {
return func(context *gin.Context) {
t := time.Now()
fmt.Println("中间件执行之前")
// 设置request变量到Context的key中,通过GET等函数可以取得
context.Set("request", "client_request")
// 发送request之前
context.Next()
// 发送request之后
// 这个context.Write是ResponseWriter,我们可以获得状态等消息
status := context.Writer.Status()
fmt.Println("中间件执行之后", status)
t2 := time.Since(t)
fmt.Println("time:", t2)
}
}
- 执行结果
jartin@macbookpro1 elastic_notes % curl localhost:80/middleware {"middle_ware_req":"client_request","request":"client_request"}% [GIN-debug] Listening and serving HTTP on :80 中间件执行之前 request: client_request 中间件执行之后 200 time: 58.854µs [GIN] 2023/10/08 - 15:36:08 | 200 | 66.165µs | 127.0.0.1 | GET "/middleware"
Next()方法
Info
我们怎么解决一个请求和一个响应经过我们的中间件呢? 神奇的语句出现了, 没错就是c.Next()
,所有中间件都有Request
和Response
的分水岭, 就是这个c.Next()
,否则没有办法传递中间件。
服务端使用Use
方法导入middleware
,当请求/middleware
来到的时候,会执行MiddleWare()
, 并且我们知道在GET注册的时候,同时注册了匿名函数,所有请看Logger函数中存在一个c.Next()
的用法,它是取出所有的注册的函数都执行一遍,然后再回到本函数中,所以,本例中相当于是先执行了 c.Next()
即注册的匿名函数,然后回到本函数继续执行, 所以本例的Print的输出顺序是: fmt.Println("before middleware")
fmt.Println("request:",request)
fmt.Println("after middleware,", status)
fmt.Println("time:", t2)
如果将c.Next()
放在fmt.Println("after middleware,", status)
后面,那么fmt.Println("after middleware,", status)
和fmt.Println("request:",request)
执行的顺序就调换了。所以一切都取决于c.Next()
执行的位置。c.Next()
的核心代码如下:
// Next只能在中间件内部使用。
// 它在调用处理程序内部执行链中的挂起处理程序。
// 请参阅GitHub中的示例。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
它其实是执行了后面所有的handlers
。
一个请求过来,Gin
会主动调用c.Next()
一次。因为handlers
是slice
,所以后来者中间件会追加到尾部。这样就形成了形如m1(m2(f()))
的调用链。正如上面数字① ②
标注的一样, 我们会依次执行如下的调用:
另外,如果没有注册就使用MustGet
方法读取c的值将会抛错,可以使用Get
方法取而代之。上面的注册装饰方式,会让所有下面所写的代码都默认使用了router
的注册过的中间件。
单个路由中间件
- 当然,gin也提供了针对指定的路由函数进行注册。
r.GET("/before", MiddleWare(), func(context *gin.Context) { req := context.MustGet("request").(string) context.JSON(http.StatusOK, gin.H{ "middle_req": req, }) }) jartin@macbookpro1 elastic_notes % curl localhost/before {"middle_req":"client_request"}% [GIN-debug] Listening and serving HTTP on :80 中间件执行之前 中间件执行之后 200 time: 79.774µs [GIN] 2023/10/09 - 14:49:06 | 200 | 91.324µs | 127.0.0.1 | GET "/before"
中间件实践
Quote
中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。
简单认证BasicAuth
Abstract
关于使用gin.BasicAuth()
middleware
, 可以直接使用一个router
group
进行处理, 本质和上面的一样。
先定义私有数据:
// 模拟私有数据
var secrets = gin.H{
"hanru": gin.H{"email": "hanru@163.com", "phone": "123433"},
"wangergou": gin.H{"email": "wangergou@example.com", "phone": "666"},
"ruby": gin.H{"email": "ruby@guapa.com", "phone": "523443"},
}
然后使用 gin.BasicAuth
中间件,设置授权用户
authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"hanru": "hanru123",
"wangergou": "1234",
"ruby": "hello2",
"lucy": "4321",
}))
最后定义路由:
定义路由
authorized.GET("/secrets", func(c *gin.Context) {
// 获取提交的用户名(AuthUserKey)
user := c.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
} else {
c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
}
})
然后启动项目,打开浏览器输入以下网址:http://127.0.0.1/admin/secrets
,然后会弹出一个登录框
需要输入正确的用户名和密码:
完整代码
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
r := gin.Default()
var secrets = gin.H{
"zhangsan": gin.H{"email": "zhangsan@163.com", "phone": "123123"},
"lisi": gin.H{"email": "lisi@163.com", "phone": "123123"},
"robin": gin.H{"email": "robin@163.com", "phone": "123123"},
}
authorization := r.Group("/admin", gin.BasicAuth(gin.Accounts{
"zhangsan": "admin123",
"lisi": "admin123",
"lucy": "admin123",
}))
authorization.GET("/login", func(context *gin.Context) {
// 获取提交的用户名 AuthUserKey
user := context.MustGet(gin.AuthUserKey).(string)
if secret, ok := secrets[user]; ok {
context.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
} else {
context.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
}
})
r.Run(":80")
}
总结
- 全局中间件
router.Use(gin.Logger())
router.Use(gin.Recovery())
- 单路由的中间件,可以加任意多个
router.GET("/benchmark", MyMiddelware(), benchEndpoint)
- 群组路由的中间件
authorized := router.Group("/", MyMiddelware())
- 或者这样用: