鉴权认证、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的很多业务都适用。
因此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效地将具体业务和系统功能进行解耦,还可以达到灵活配置的目的。
这种通用业务独立开发并灵活配置使用的组件,一般称之为"中间件"。其就相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。
每个用户都有自己的业务,Web框架不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义通用功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。这个也就是我们所说的中间件。
通俗点理解,中间件就是个函数。比如前缀是/admin来访问的都需要进行鉴权认证,而Web框架又没有实现这个功能。
这时可能想到直接在该路由的处理函数加上这个功能就行啦。但是要是有很多前缀是/admin的路由,那就要写很多很多,这是很繁琐的。
中间件的使用
那我们就想有这种效果,这种就写的很舒服了,gin框架就是这样使用的。
//鉴权中间件
func AuthMiddleWare() gee.HandlerFunc {
........
}
func main() {
r := gee.New()
v2 := r.Group("/admin")
v2.Use(AuthMiddleWare()) //该路由组使用鉴权中间件,也只有该组使用这个中间件
v2.GET("/home", func(c *gee.Context) {
//AuthMiddleWare()
c.JSON(http.StatusOK, gin.H{
"msg": "home路由",
})
})
v2.GET("/ok", func(c *gee.Context) {
//AuthMiddleWare()
c.JSON(http.StatusOK, gin.H{
"msg": "ok路由",
})
})
}
中间件的设计
前面说了中间件是个函数,那可以设置成映射的 Handler 一致,处理的输入是Context
对象。
type HandlerFunc func(*Context)
一个路由组可以会使用多个中间件的,上一节中已在路由组的结构中添加了中间件数组。那来实现下添加中间件方法Use。该方法可以一次添加多个中间件。
RouterGroup struct {
prefix string
middlewares []HandlerFunc // support middleware
parent *RouterGroup // support nesting
engine *Engine // all groups share a Engine instance
}
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
简单想法:一条直线处理
我们想的简单的版本,前后添加了中间件A,中间件B。那处理的流程就是:中间件A -----> 中间件B -----> 路由处理函数。
回到我们的执行路由处理函数部分,那么ServeHTTP就需要改动下。
//之前的
//func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// c := newContext(w, req)
// engine.router.handle(c)
//}
//需要执行中间件的做法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := newContext(w, req)
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
group.middlewares(c) //执行中间件
}
}
engine.router.handle(c)
}
这种实现起来是很简单,但却不实用的。就像是要鉴权认证,假如认证失败后,那肯定是不执行路由处理函数的,而这个做法却是继续执行的。
还有有些中间件想在路由处理函数执行后做一些操作,例如计算响应时长之类的。 那就要等路由处理函数执行后才可以计算,当前实现的做法是不妥的,不能实现这样功能的。所以需要继续改善。
我们想要的中间件是支持用户在请求被处理的前后,做一些额外的操作。
改善
现在来看一个中间件,日志中间件。
假如我们要计算响应时长,那就是要等路由Handler处理完之后才能执行第八行代码。所以需要做些操作。
那在第六行中,c.Next()
表示执行其他的中间件或用户的Handler。
等执行完其他中间件和用户的Handler后,再执行第八行代码,这样就可以符合我们想记录响应时长的要求了。
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
//鉴权中间件
func AuthMiddleWare() HandlerFunc {
........
}
func main() {
r := gee.New()
r.Use(Logger())
v2 := r.Group("/admin")
v2.Use(AuthMiddleWare()) //该路由组使用鉴权中间件,也只有该组使用这个中间件
v2.GET("/home", func(c *gee.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "home路由",
})
})
}
那么重点就是在c.Next()中了。c.Next()可以执行中间件,那说明Context保存了该中间件。所以,Context结构体添加了中间件切片midH
andlers和index。
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
Params map[string]string
// response info
StatusCode int
// middleware
midHandlers []HandlerFunc //这节新添加的
index int8 //这节新添加的
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Wrtier: w,
Req: req,
Path: req.URL.Path,
Method: req.Method,
index: -1, //这节新添加的
}
}
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.midHandlers)) {
c.midHandlers[c.index](c) //执行中间件
c.index++
}
}
index
是记录当前执行到第几个中间件,当在中间件中调用Next
方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next
方法之后定义的部分。
如果我们将用户在映射路由时定义的Handler
添加到c.handlers
列表中,结果会怎么样呢?
func A(c *Context) {
part1 //可以是记录开始时间
c.Next()
part2 //可以是记录整个响应时长
}
func B(c *Context) {
part3
c.Next()
part4
}
假设我们应用了中间件 A 和 B,和路由映射的 Handler。那么c.handlers
是这样的:[A, B, Handler],c.index
初始化为-1。
先执行中间件A,那就是先执行到part1。之后到c.Next(),这时候就到执行c.handlers中的下一个,即是中间件B,那就是执行part3。之后执行c.Next(),这时就执行到路由Hander,跟着就到了part4。part4结束后,即是中间件中的c.Next()结束了,最终执行part2.
index是怎么变化的要到后面才好说明白。
最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2
。恰恰满足了我们对中间件的要求。
接着就看回到ServeHTTP方法。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// c := newContext(w, req)
// engine.router.handle(c)
var middlewares []HandlerFunc
for _, group := range engine.gorups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...) //添加该路由组的中间件
}
}
c := newContext(w, req)
c.midHandlers = middlewares //路由组中间件都赋值给Context结构体的midHandlers
engine.router.handle(c)
}
那之后肯定是在engine.router.handle(c)中有些变化的了。
最后的c.Next()是很重要的,这样操作后,所有的中间件和路由Handler都统一在Next()中执行的了。
这里可以把之前例子里的index的疑惑讲明白了。c.index默认初始化是-1的,这里执行C.Next(),c.index就变成0了,就可以执行c.handler[0],即是中间件A。
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
key := c.Method + "-" + n.path
c.Params = params
c.midHandlers = append(c.midHandlers, r.handers[key]) //添加路由Handler到midHandlers中
} else {
c.midHandlers = append(c.midHandlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
}
c.Next() //从这里开始执行第一个中间件,要是没有中间件就直接执行路由Handler
//上一节的做法
//n, params := r.getRoute(c.Method, c.Path)
// if n != nil {
// c.Params = params
// // key := c.Method + "-" + c.Path
// key := c.Method + "-" + n.path
// fmt.Println(key)
// r.handers[key](c)
// } else {
// c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
// }
}
说完了c.Next()后,还有c.Abort()方法。
在进行鉴权认证不通过后,后面的中间件和路由Handler也就不需要执行的了。
在c.Next()中是通过c.index来进行判断执行哪个中间件的。首先我们定义AbortIndex常量,主要是用于控制框架处理请求链路的长度,即是中间件和路由Handler总个数。这时候设置c.index=AbortIndex,那么c.Next()就不会执行中间件了。那么我们在之前使用Use添加中间件的时候,就需要判断中间件个数是否超过了最大个数,这里就不展示了。
const AbortIndex = math.MaxInt8 >> 1
func (c *Context) Abort() {
c.index = AbortIndex
}
测试
场景:/admin的使用鉴权认证中间件,全局的使用日志中间件。
鉴权失败的话,调用c.Abort()
func authMiddleWare() gee.HandlerFunc {
return func(c *gee.Context) {
fmt.Println("start 鉴权中间件")
token := c.Req.Header.Get("token")
if token == "" || token != "123" {
c.JSON(http.StatusUnauthorized, gee.H{"message": "身份验证失败"})
c.Abort()
//return //不使用return的话,后面的fmt.Println("hello")会执行,使用return的话,后面的不会执行
} else {
fmt.Println("鉴权成功")
}
//使用Abort()后要是不使用return,打印hello还是会执行的
//fmt.Println("hello")
}
}
func main() {
r := gee.New()
r.Use(gee.Logger())
r.GET("/home", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Index home</h1>")
})
v1 := r.Group("/admin")
v1.Use(authMiddleWare())
{
v1.GET("/myname", func(c *gee.Context) {
c.String(http.StatusOK, "Hello li")
})
}
r.Run("localhost:10000")
}
结果
完整代码:https://github.com/liwook/Go-projects/tree/main/gee-web/5-Middlewares