gin中间件

gin是golang写的轻量级web框架,这里不介绍gin的基本使用方法,这里主要看下中间件(Middleware)的使用和gin的代码实现。

1. Middleware

中间件也叫拦截器或者过滤器,本质上都是在一个http请求被处理之前执行的一段代码,gin的中间件是一个函数,函数签名和gin的路由处理函数一致,都是func(*Context)类型。

1.1 注册中间件

func startServer() {
    port := os.Getenv("GIN_PORT")
    if port == "" {
        port = ":10080"
    }
    router := gin.New()
    router.Use(gin.Logger(), gin.Recovery())
    router.GET("/index", func(context *gin.Context) {
        context.String(http.StatusOK, "ok")
    })
    router.Run(port)
}

func main() {
    startServer()
}

说明:

  1. gin.New()返回一个没有注册中间件的gin.Engine对象,gin.Default()默认注册Logger()和Recovery()中间件
  2. 手动注册Logger()中间件并启动http服务器

1.1 gin.New()

gin.New()创建一个gin.Egine实例router,并初始化router的匿名成员RouterGroup,因为RouterGroup可以嵌套,所以routerRouterGroup就是最外层RouterGroup。初始化代码如下:

func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,
            basePath: "/",
            root:     true,
        },
        FuncMap:                template.FuncMap{},
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
        TrustedPlatform:        defaultPlatform,
        UseRawPath:             false,
        RemoveExtraSlash:       false,
        UnescapePathValues:     true,
        MaxMultipartMemory:     defaultMultipartMemory,
        trees:                  make(methodTrees, 0, 9),
        delims:                 render.Delims{Left: "{{", Right: "}}"},
        secureJSONPrefix:       "while(1);",
        trustedProxies:         []string{"0.0.0.0/0"},
        trustedCIDRs:           defaultTrustedCIDRs,
    }
    engine.RouterGroup.engine = engine
    engine.pool.New = func() interface{} {
        return engine.allocateContext()
    }
    return engine
}

如下是RouterGroup类型定义:

type RouterGroup struct {
    Handlers HandlersChain  // 中间件组成的切片
    basePath string  // 路由组的基础路径
    engine   *Engine  // 这个RouterGroup实例所属的Egine对象,可以看做父子关系中的父RouterGroup
    root     bool  // 是否为最外层的RouterGroup,其实就是是否为最终创建的Egine对象
}

可知router对象的RouterGroup成员被初始化为:

Handlers为nil,意味着还未设置中间件函数
basePath为/,就是根路径
engine为nil,因为它本身就是最外层的RouterGroup
root为true,因为它本身就是最外层的RouterGroup

1.2 注册中间件

实例中通过router.Use(gin.Logger(), gin.Recovery())注册了两个中间件,Use()方法其实是RouterGroup类型的方法,因为Egine类型包含一个RouterGroup的匿名成员,所以Egine类型的对象也能调用Use()方法,看代码实现:

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

就是把参数中的中间件追加到router对象的RouterGroup成员,现在RouterGroup成员的Handlers成员包含两个中间件gin.Logger(),gin.Recovery()

1.3 注册路由处理函数

    router.GET("/index", func(context *gin.Context) {
        context.String(http.StatusOK, "ok")
    })

看具体代码实现:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    absolutePath := group.calculateAbsolutePath(relativePath)
    handlers = group.combineHandlers(handlers)
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

代码比较简单:

  1. 计算绝对路径,其实就是拼接RouterGroup成员的basePath成员和参数relativePath
  2. RouterGroup中的中间件和参数handlers合并成更大的切片,中间件在前
  3. 把真实的路径和处理函数添加到路由树

值得注意的是拼接路径后gin会自动清理路径,比如上面的路径实际拼接是//index,gin自动清理成/index/index的处理函数最终也变成了3个,其中前2个来自中间件。

注意: gin用httprouter路由树实现,但是也有点区别,httprouter的handler是一个函数,gin中是一个函数切片

2. 处理http请求

gin的Egine类型实现了ServeHTTP(w, r)方法,看具体实现:

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context)
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c)

    engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    unescape := false
    if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
        rPath = c.Request.URL.RawPath
        unescape = engine.UnescapePathValues
    }

    if engine.RemoveExtraSlash {
        rPath = cleanPath(rPath)
    }

    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        if t[i].method != httpMethod {
            continue
        }
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        if value.params != nil {
            c.Params = *value.params
        }
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        if httpMethod != "CONNECT" && rPath != "/" {
            if value.tsr && engine.RedirectTrailingSlash {
                redirectTrailingSlash(c)
                return
            }
            if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                return
            }
        }
        break
    }

    if engine.HandleMethodNotAllowed {
        for _, tree := range engine.trees {
            if tree.method == httpMethod {
                continue
            }
            if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
                c.handlers = engine.allNoMethod
                serveError(c, http.StatusMethodNotAllowed, default405Body)
                return
            }
        }
    }
    c.handlers = engine.allNoRoute
    serveError(c, http.StatusNotFound, default404Body)
}

gin的ServeHTTP(w,r)和httprouter中ServeHTTP(w, r)实现基本一致,只不过gin引入了一个Context类型用来包装:

  1. 创建一个Context对象,并把w, req成员设置到Context内部
  2. 根据http请求的方法找到路由树根节点,并通过path找对应的handlers,如果查找失败则返回错误
  3. 如果找到handlers则把handlers设置到Context内部成员,并执行Context的Next()方法,接下来看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 c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

Next()方法其实就是遍历匹配到的handlers,并依次执行,所以访问/index时每次实际上都执行三个函数,先执行两个中间件函数后执行路由处理函数。
有意思的是在中间件中调用Next()方法,就变成中间件递归调用Next()方法,下面是gin.Logger()中间件主要实现:

    return func(c *Context) {
        // Start timer
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery

        // Process request
        c.Next()

        // Log only when path is not being skipped
        if _, ok := skip[path]; !ok {
            param := LogFormatterParams{
                Request: c.Request,
                isTerm:  isTerm,
                Keys:    c.Keys,
            }

            // Stop timer
            param.TimeStamp = time.Now()
            param.Latency = param.TimeStamp.Sub(start)

            param.ClientIP = c.ClientIP()
            param.Method = c.Request.Method
            param.StatusCode = c.Writer.Status()
            param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()

            param.BodySize = c.Writer.Size()

            if raw != "" {
                path = path + "?" + raw
            }

            param.Path = path
            fmt.Fprint(out, formatter(param))
        }
    }

在执行Next()函数时先执行中间件,所以如下代码会先执行:

        // Start timer
        start := time.Now()
        path := c.Request.URL.Path
        raw := c.Request.URL.RawQuery

然后进入递归调用Next(),这时会先执行gin.Recovery()中间件再执行/index处理函数,这些执行完才回到gin.Logger()剩下的部门,所以中间件中c.Next()之前的代码比路由处理函数先执行,c.Next()之后的代码在路由处理函数执行完后才执行,所以gin.Logger()才能计算处理一个http请求的时间和获取http的状态码。下面一段代码能更好的演示多个中间件中调用Next()方法的执行顺序:

func LogMiddleware(context *gin.Context) {
    fmt.Println("LogMiddleware start")
    context.Next()
    fmt.Println("LogMiddleware end")
}

func TestMiddleware(context *gin.Context) {
    fmt.Println("TestMiddleware start")
    context.Next()
    fmt.Println("TestMiddleware end")
}

func startServer() {
    port := os.Getenv("GIN_PORT")
    if port == "" {
        port = ":10080"
    }
    router := gin.New()
    router.Use(LogMiddleware, TestMiddleware)
    router.GET("/index", func(context *gin.Context) {
        fmt.Println("process /index")
        context.String(http.StatusOK, "ok")
    })
    router.Run(port)
}
func main() {
    startServer()
}

访问/index路径,结果如下:

image.png

2. BasicAuth()中间件

假如用户需要通过认证才能访问/account/submit,/account/info,则可以创建一个RouterGroup,并且给这个RouterGroup设置一个中间件去验证用户身份,gin提供BasicAuth()中间件来实现该功能。

var myAccounts = map[string]string {
    "jordan": "23",
    "kobe":"24",
    "wade":"3",
}

func LogMiddleware(context *gin.Context) {
    fmt.Println("LogMiddleware start")
    context.Next()
    fmt.Println("LogMiddleware end")
}

func TestMiddleware(context *gin.Context) {
    fmt.Println("TestMiddleware start")
    context.Next()
    fmt.Println("TestMiddleware end")
}

func startServer() {
    port := os.Getenv("GIN_PORT")
    if port == "" {
        port = ":10080"
    }
    router := gin.New()
    router.Use(LogMiddleware, TestMiddleware)
    group := router.Group("/account", gin.BasicAuth(myAccounts))
    group.GET("/submit", func(context *gin.Context) {
        context.String(http.StatusOK, "submit success")
    })
    group.GET("/info", func(context *gin.Context) {
        context.String(http.StatusOK, "ok")
    })
    router.Run(port)
}

func main() {
    startServer()
}

注意group是从router创建的,所以group会包含router的中间件,/account/submit,/account/info的处理函数有4个,依次为gin.Logger(), gin.Recovery(), gin.BasicAuth()和用户设置的handler。
设置BasicAuth()中间件,把传入的myAccounts中的成员当做user,password,并把user + ":" + password作base64转换保存起来:

func processAccounts(accounts Accounts) authPairs {
    length := len(accounts)
    assert1(length > 0, "Empty list of authorized credentials")
    pairs := make(authPairs, 0, length)
    for user, password := range accounts {
        assert1(user != "", "User can not be empty")
        value := authorizationHeader(user, password)
        pairs = append(pairs, authPair{
            value: value,
            user:  user,
        })
    }
    return pairs
}

func authorizationHeader(user, password string) string {
    base := user + ":" + password
    return "Basic " + base64.StdEncoding.EncodeToString(bytesconv.StringToBytes(base))
}

myAccounts转换后的结果如下:

"jordan": Basic d2FkZToz
"kobe": Basic am9yZGFuOjIz
"wade": Basic a29iZToyNA==

当对/account/submit,/account/info请求时,中间件判断http头中Authorization字段的值是否能匹配myAccounts转换后的字段,如果能则通过,匹配不成功则返回401。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容