命令记不住

登陆

Gin 框架基础 (二)

Gin框架middileWare

1.Gin Middleware

中间件Middleware

Go的net/http设计的一大特点就是特别容易构建中间件。 gin也提供了类似的中间件。需要注意的是中间件只对注册过的路由函数起作用。对于分组路由,嵌套使用中间件,可以限定中间件的作用范围。中间件分为全局中间件,单个路由中间件和群组中间件。
我们之前说过, Context 是 Gin 的核心, 它的构造如下:

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 . 而它的签名正是

type HandlerFunc func(*Context)

所以中间件和我们普通的 HandlerFunc 没有任何区别。我们怎么写 HandlerFunc 就可以怎么写一个中间件。

2.中间件的注册方式

2.1 三种注册方式(全局注册,局部注册,路由组注册)

先定义一个中间件函数

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"time"
)

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上下文添加一个属性,并赋值。后面的路由处理器,可以根据被中间件装饰后提取其值。需要注意,虽然名为全局中间件,只要注册中间件的过程之前设置的由,将不会受注册的中间件所影响。只有注册了中间件以下代码的路由函数规则,才会被中间件装饰。

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进行路由,都等于被装饰了。想要区分权限范围,可以使用组返回的对象注册中间件。

运行项目,可以在浏览器访问 或者 终端输入命令进行访问

curl http://127.0.0.1:8080/middleware

2.2 路由的多种注册方式

常规三种注册方式

全局注册,局部注册,路由组注册

func main() {
	r := gin.Default()
	//注册全局中间件
	r.Use(m1, m2)
	r.GET("/index", m1, indexHandler, m2)
	r.GET("/home", m2, homeHandler)

	shopGroup := r.Group("/shop")

	shopGroup.Use(m1, m2)

	{
		shopGroup.GET("/index", m1, m2, func(c *gin.Context) {
			c.String(http.StatusOK, "/shop/index")
		})
	}
	r.Run(":8081")

}

闭包方式注册路由

//闭包:一个中间件实现两个作用,返回值是handlerfunc类型
func closure(flag bool) func(c *gin.Context) {
	return func(c *gin.Context) {
		if flag {
			//登陆校验
			fmt.Println("进行登陆校验")
		} else {
			fmt.Println("不用登陆校验")
		}
	}
}

3.三个方法

c.Abort

c.Next

c.Set/c.Get

Next()方法
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

我们怎么解决一个请求和一个响应经过我们的中间件呢?神奇的语句出现了, 没错就是 c.Next() ,所有中间件都有 Request 和 Response 的分水岭, 就是这个 c.Next() ,否则没有办法传递中间件。

1. Next()是中间件中请求和响应的分界点
package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func m1(c *gin.Context) {
	fmt.Println("m1;before c.Next()")
	c.Next()
	fmt.Println("m1:after c.Next()")
}
func m2(c *gin.Context) {
	fmt.Println("m2;before c.Next()")
	//判断用户是否登陆成功
	//如果登陆不成功就不让请求继续往后走
	c.String(http.StatusOK, "请先邓丽")
	c.Abort()
	fmt.Println("m2:after c.Next()")
}

func indexHandler(c *gin.Context) {
	fmt.Println("index  Handler")
	c.String(http.StatusOK, "index")
}

func main() {
	r := gin.Default()
	//注册全局中间件
	//r.Use(m1, m2)
	r.GET("/index", m1, indexHandler, m2)
	//r.GET("/home", m2, homeHandler)
	r.Run(":8081")

}

执行结果

PS C:\Users\vSphere\go\src\proetcd\middlerware\04> go run  .\next.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /index                    --> main.m2 (5 handlers)
[GIN-debug] Listening and serving HTTP on :8081
m1;before c.Next()
index  Handler
m2;before c.Next()
m2:after c.Next()
m1:after c.Next()
[GIN] 2020/11/09 - 20:28:27 | 200 |       976.9µs |       127.0.0.1 | GET      "/index"

服务端使用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 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②

另外,如果没有注册就使用 MustGet 方法读取c的值将会抛错,可以使用Get方法取而代之。上面的注册装饰方式,会让所有下面所写的代码都默认使用了router的注册过的中间件。

2. Abort() 立刻终止

将Next()请求链条终止,不想执行后续的处理方法,做登陆校验

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func m1(c *gin.Context) {
	fmt.Println("m1;before c.Next()")
	//c.Next() 是中间件请求和响应的分界点
	c.Abort()
	c.AbortWithStatus(200)
	fmt.Println("m1:after c.Next()")
}
func m2(c *gin.Context) {
	fmt.Println("m2;before c.Next()")
	c.Next()
	fmt.Println("m2:after c.Next()")
}

func indexHandler(c *gin.Context) {
	fmt.Println("index  Handler")
	c.String(http.StatusOK, "index")
}

func main() {
	r := gin.Default()
	//注册全局中间件
	//r.Use(m1, m2)
	r.GET("/index", m1, indexHandler, m2)
	//r.GET("/home", m2, homeHandler)
	r.Run(":8081")

}

运行结果

PS C:\Users\vSphere\go\src\proetcd\middlerware\04> go run  .\next.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /index                    --> main.m2 (5 handlers)
[GIN-debug] Listening and serving HTTP on :8081
m1;before c.Next()
m1:after c.Next()
[GIN] 2020/11/09 - 20:57:39 | 200 |            0s |       127.0.0.1 | GET      "/index"

AbortWithStatus()返回响应码

3. Set/Get

请求文链条中转递值

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func m2(c *gin.Context) {
	c.Set("username", "opxdev")
	fmt.Println("m2:after  c.Next()")
}

func indexHandler(c *gin.Context) {
	fmt.Println("index  Handler")
	username, ok := c.Get("username")
	if !ok {
		username = "匿名用户"
	}
	c.String(http.StatusOK, fmt.Sprintf("hello %v", username))
}

func main() {
	r := gin.Default()
	r.GET("/index", indexHandler, m2)
	r.Run(":8081")

}

运行结果

PS C:\Users\vSphere\go\src\proetcd\middlerware\04> go run  .\next.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /index                    --> main.m2 (4 handlers)
[GIN-debug] Listening and serving HTTP on :8081
index  Handler
m2:after  c.Next()
[GIN] 2020/11/09 - 21:22:59 | 200 |       977.3µs |       127.0.0.1 | GET      "/index"

hello 匿名用户

4.单个路由中间件

当然, gin也提供了针对指定的路由函数进行注册。

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 被装饰了中间件。

5.中间件实践(鉴权)

中间件最大的作用,莫过于用于一些记录log,错误handler,还有就是对部分接口的鉴权。下面就实现一个简易的鉴权中间件。

简单认证BasicAuth

关于使用gin.BasicAuth() middleware,可以直接使用一个router group进行处理,本质和上面的一样。

先定义私有数据:

// 模拟私有数据
var secrets = gin.H{
	"ls":   gin.H{"email": "[email protected]", "phone": "123456"},
	"yang": gin.H{"email": "[email protected]", "phone": "111111"},
	"edu":  gin.H{"email": "[email protected]", "phone": "666666"},
}

然后使用 gin.BasicAuth 中间件,设置授权用户

authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{
	"ls":   "123",
	"yang": "111",
	"edu":  "666",
	"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:8080/admin/secrets 然后弹出的登录框输入正确的用户名和密码。

6.启动多个服务

我们可以在多个端口启动服务,例如

使用goroutine开启多个端口

func main() {
	r := gin.Default()
	r.GET("/index", indexHandler, m2)

	go r.Run(":9090")

	r.Run(":8081")

}

使用第三方库:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/router01", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 01",
			},
		)
	})
	return e
}
func router02() http.Handler {
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/router02", func(c *gin.Context) {
		c.JSON(
			http.StatusOK,
			gin.H{
				"code":  http.StatusOK,
				"error": "Welcome server 02",
			},
		)
	})
	return e
}
func main() {
	server01 := &http.Server{
		Addr:         ":8080",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	server02 := &http.Server{
		Addr:         ":8081",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
	// 借助errgroup.Group或者⾃自⾏行行开启两个goroutine分别启动两个服务
	g.Go(func() error {
		return server01.ListenAndServe()
	})
	g.Go(func() error {
		return server02.ListenAndServe()
	})
	if err := g.Wait(); err != nil {
		log.Fatal(err)
	}
}
推荐阅读