💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
### GoWeb开发实战】Gin框架\_中间件 # 中间件middleware golang的net/http设计的一大特点就是特别容易构建中间件。gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。 我们之前说过,`Context`是`Gin`的核心, 它的构造如下: ~~~go type Context struct { writermem responseWriter Request *http.Request Writer ResponseWriter Params Params handlers HandlersChain index int8 engine *Engine Keys map[string]interface{} Errors errorMsgs Accepted []string } ~~~ 其中`handlers`我们通过源码可以知道就是`[]HandlerFunc`. 而它的签名正是: ~~~go type HandlerFunc func(*Context) ~~~ 所以中间件和我们普通的`HandlerFunc`没有任何区别对吧, 我们怎么写`HandlerFunc`就可以怎么写一个中间件. ## 一、全局中间件 先定义一个中间件函数: ~~~go 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) } } ~~~ 该函数很简单,只会给c上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的路由,将不会受注册的中间件所影响。只有注册了中间件一下代码的路由函数规则,才会被中间件装饰。 ~~~go 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(":8080") ~~~ 使用router装饰中间件,然后在`/middlerware`即可读取request的值,注意在`router.Use(MiddleWare())`代码以上的路由函数,将不会有被中间件装饰的效果。 > 使用花括号包含被装饰的路由函数只是一个代码规范,即使没有被包含在内的路由函数,只要使用router进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。 运行项目,可以在终端输入命令进行访问, ~~~shell hanru:~ ruby$ curl http://127.0.0.1:8080/middleware ~~~ ![gin_yunxing36](http://image.chaindesk.cn/gin_yunxing36.png/mark) 或者是在浏览器中输入网址进行访问: ![gin_yunxing37](http://image.chaindesk.cn/gin_yunxing37.png/mark) 然后在服务器端的运行结果如下: ![gin_yunxing38](http://image.chaindesk.cn/gin_yunxing38.png/mark) ## 二、Next()方法 我们怎么解决一个请求和一个响应经过我们的中间件呢?神奇的语句出现了, 没错就是`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()的核心代码如下: ~~~go // Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. // See example in GitHub. func (c *Context) Next() { c.index++ for s := int8(len(c.handlers)); c.index < s; c.index++ { c.handlers[c.index](c) } } ~~~ 它其实是执行了后面所有的handlers。 一个请求过来,`Gin`会主动调用`c.Next()`一次。因为`handlers`是`slice`,所以后来者中间件会追加到尾部。这样就形成了形如`m1(m2(f()))`的调用链。正如上面数字① ② 标注的一样, 我们会依次执行如下的调用: ~~~ m1① -> m2① -> f -> m2② -> m1② ~~~ ![gin-middleware](http://image.chaindesk.cn/gin-middleware.png/mark) 另外,如果没有注册就使用`MustGet`方法读取c的值将会抛错,可以使用Get方法取而代之。上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。 ## 二、单个路由中间件 当然,gin也提供了针对指定的路由函数进行注册。 ~~~go router.GET("/before", MiddleWare(), func(c *gin.Context) { request := c.MustGet("request").(string) c.JSON(http.StatusOK, gin.H{ "middile_request": request, }) }) ~~~ 把上述代码写在 router.Use(Middleware())之前,同样也能看见`/before`被装饰了中间件。通过浏览器访问以下地址: ![gin_yunxing39](http://image.chaindesk.cn/gin_yunxing39.png/mark) ## 三、中间件实践 中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。 ### 3.1 简单认证BasicAuth 关于使用gin.BasicAuth() middleware, 可以直接使用一个router group进行处理, 本质和上面的一样。 先定义私有数据: ~~~go // 模拟私有数据 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 中间件,设置授权用户 ~~~go authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ "hanru": "hanru123", "wangergou": "1234", "ruby": "hello2", "lucy": "4321", })) ~~~ 最后定义路由: ~~~go 定义路由 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:8080/admin/secrets,然后会弹出一个登录框:!\[gin\_yunxing40\](http://image.chaindesk.cn/gin\_yunxing40.png/mark](http://127.0.0.1:8080/admin/secrets%EF%BC%8C%E7%84%B6%E5%90%8E%E4%BC%9A%E5%BC%B9%E5%87%BA%E4%B8%80%E4%B8%AA%E7%99%BB%E5%BD%95%E6%A1%86%EF%BC%9A![gin_yunxing40](http://image.chaindesk.cn/gin_yunxing40.png/mark)) 需要输入正确的用户名和密码: ![gin_yunxing41](http://image.chaindesk.cn/gin_yunxing41.png/mark) ## 四、总结 / 1.全局中间件 router.Use(gin.Logger()) router.Use(gin.Recovery()) // 2.单路由的中间件,可以加任意多个 router.GET("/benchmark", MyMiddelware(), benchEndpoint) // 3.群组路由的中间件 authorized := router.Group("/", MyMiddelware()) // 或者这样用: authorized := router.Group("/") authorized.Use(MyMiddelware()) {​ authorized.POST("/login", loginEndpoint) }