Middlewares

各类常用的中间件

  • basic auth HTTP Basic Auth,建议只用于内部管理系统使用

  • body parser 请求数据的解析中间件,支持application/json以及application/x-www-form-urlencoded两种数据类型

  • cache HTTP缓存模块,基于响应头的Cache-Control缓存数据,并支持缓存时以br或gzip压缩后缓存

  • compress 数据压缩中间件,默认仅支持gzip。如果需要支持更多的压缩方式,如brotli、snappy、zstd以及lz4,可以使用elton-compress,也可根据需要增加相应的压缩处理

  • concurrent limiter 根据指定参数限制并发请求,可用于订单提交等防止重复提交或限制提交频率的场景

  • error handler 用于将处理函数的Error转换为对应的响应数据,如HTTP响应中的状态码(4xx, 5xx),对应的出错类别等,建议在实际使用中根据项目自定义的Error对象生成相应的响应数据

  • etag 用于生成HTTP响应数据的ETag

  • fresh 判断HTTP请求是否未修改(Not Modified)

  • json picker 用于从响应的JSON中筛选指定字段

  • jwt jwt中间件

  • logger 生成HTTP请求日志,支持从请求头、响应头中获取相应信息

  • proxy Proxy中间件,可定义请求转发至其它的服务

  • recover 捕获程序的panic异常,避免程序崩溃

  • renderer 模板渲染中间件,用于将模板编译输出为html

  • responder 响应处理中间件,用于将Context.Body(interface{})转换为对应的JSON数据并输出。如果系统使用xml等输出响应数据,可参考此中间件实现interface{}至xml的转换

  • response-size-limiter 响应长度限制中间件,用于限制响应数据的最大长度

  • router-concurrent-limiter 路由并发限制中间件,可以针对路由限制并发请求量。

  • session Session中间件,默认支持保存内存中,可自定义相应的存储实现保存至redis等数据库。

  • stats 请求处理的统计中间件,包括处理时长、状态码、响应数据长度、连接数等信息

  • static serve 静态文件处理中间件,默认支持从目录中读取静态文件或实现StaticFile的相关接口,从packr或者数据库(mongodb)等读取文件

  • tracker 可以用于在POST、PUT等提交类的接口中增加跟踪日志,此中间件将输出QueryString,Params以及RequestBody部分,并能将指定的字段做"***"的处理,避免输出敏感信息

basic auth

HTTP basic auth中间件,提供简单的认证方式,建议只用于内部管理系统。

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
	"github.com/vicanso/hes"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewBasicAuth(middleware.BasicAuthConfig{
		Validate: func(account, pwd string, c *elton.Context) (bool, error) {
			if account == "tree.xie" && pwd == "password" {
				return true, nil
			}
			if account == "n" {
				return false, hes.New("account is invalid")
			}
			return false, nil
		},
	}))

	e.GET("/", func(c *elton.Context) (err error) {
		c.BodyBuffer = bytes.NewBufferString("hello world")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

body parser

解析HTTP请求接收到的数据,支持jsonform的提交,可以根据应用场景增加各类Decoder以支持更多的数据类型,如提交数据的gzip解压等。

NewDefaultBodyParser

创建一个默认的body parser中间件,它包括gzipjson的处理。首先根据提交数据的Content-Encoding是否为gzip,如果是则先解压,再判断数据是否json

e.Use(middleware.NewDefaultBodyParser())

NewGzipDecoder

创建一个gzip数据的decoder

conf := middleware.BodyParserConfig{}
conf.AddDecoder(middleware.NewGzipDecoder())
e.Use(middleware.NewBodyParser(conf))

NewJSONDecoder

创建一个json数据的decoder

conf := middleware.BodyParserConfig{}
conf.AddDecoder(middleware.NewJSONDecoder())
e.Use(middleware.NewBodyParser(conf))

NewFormURLEncodedDecoder

创建一个form数据的decoder(不建议使用)

conf := middleware.BodyParserConfig{
	ContentTypeValidate: middleware.DefaultJSONAndFormContentTypeValidate
}
conf.AddDecoder(middleware.NewFormURLEncodedDecoder())
e.Use(middleware.NewBodyParser(conf))

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewDefaultBodyParser())

	e.POST("/user/login", func(c *elton.Context) (err error) {
		c.BodyBuffer = bytes.NewBuffer(c.RequestBody)
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

cache

缓存中间件,对于GETHEAD的请求,根据其Cache-Control判断是否可缓存,若可缓存则将数据缓存至store中,下次相同的请求直接从缓存中读取。缓存数据可指定数据压缩后缓存,并响应时根据客户端自动返回压缩或未压缩数据。需要注意当前基本所有浏览器均支持br压缩,但是浏览器只在https模式下才会设置支持br,因此服务仅运行在http上,则建议使用gzip压缩。

  • 请求的缓存key默认为Method + RequestURI

  • fetch状态则表示无缓存时的请求,获取响应数据后判断是否可缓存,如果可缓存则设置缓存数据(状态:hit,数据:响应头及响应数据),不可缓存则设置缓存数据(状态:hit-for-pass,数据:空)

  • hit-for-pass状态表示该请求有相应缓存,但该缓存表示该请求不可读取缓存

  • hit状态表示该请求有相应缓存,则缓存数据可用,直接使用缓存返回客户端

  • 如果有设置压缩,缓存数据若符合压缩条件则压缩后缓存,若不符合,则缓存原始数据。响应时需要客户端是否可接受压缩数据,若可以则直接返回压缩数据,若不可以则解压后返回

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()
	// 使用redis实现的store
	e.Use(middleware.NewDefaultCache(redisStore))

	e.GET("/", func(c *elton.Context) (err error) {
		c.CacheMaxAge(time.Minute)
		c.BodyBuffer = bytes.NewBuffer(c.RequestBody)
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

compress

响应数据压缩中间件,可对特定数据类型、数据长度的响应数据做压缩处理。默认支持gzipbrotli压缩,可扩展更多的压缩方式,如:lz4,zstd等。

Compressor

实现自定义的压缩主要实现三下方法:

  • Accept 判断该压缩是否支持该压缩,根据请求头以及响应数据大小

  • Compress 数据压缩方法

  • Pipe 数据Pipe处理

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()

	e.Use(middleware.NewDefaultCompress())

	e.Use(middleware.NewDefaultResponder())

	e.GET("/", func(c *elton.Context) error {
		b := new(bytes.Buffer)
		for i := 0; i < 1000; i++ {
			b.WriteString("Hello, World!")
		}
		c.Body = &struct {
			Message string
		}{
			b.String(),
		}
		return nil
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

global concurrent limiter

全局的并发请求限制,可以用于控制应用的并发请求量。

Example

package main

import (
	"bytes"
	"sync"
	"time"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()
	e.Use(middleware.NewGlobalConcurrentLimiter(middleware.GlobalConcurrentLimiterConfig{
		Max: 1000,
	}))

	e.POST("/login", func(c *elton.Context) (err error) {
		time.Sleep(3 * time.Second)
		c.BodyBuffer = bytes.NewBufferString("hello world")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

concurrent limiter

并发请求限制,可以通过指定请求的参数,如IP、query的字段或者body等获取,限制同时并发性的提交请求,主要用于避免相同的请求多次提交。指定的Key分为以下几种:

  • :ip 客户的RealIP

  • h:key 从HTTP请求头中获取key的值

  • q:key 从HTTP的query中获取key的值

  • p:key 从路由的params中获取key的值

  • 其它的则从HTTP的Post data中获取key的值(只支持json)

Example

package main

import (
	"bytes"
	"sync"
	"time"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()
	m := new(sync.Map)
	limit := middleware.NewConcurrentLimiter(middleware.ConcurrentLimiterConfig{
		Keys: []string{
			":ip",
			"h:X-Token",
			"q:type",
			"p:id",
			"account",
		},
		Lock: func(key string, c *elton.Context) (success bool, unlock func(), err error) {
			_, loaded := m.LoadOrStore(key, true)
			// the key not exists
			if !loaded {
				success = true
				unlock = func() {
					m.Delete(key)
				}
			}
			return
		},
	})

	e.POST("/login", limit, func(c *elton.Context) (err error) {
		time.Sleep(3 * time.Second)
		c.BodyBuffer = bytes.NewBufferString("hello world")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

error handler

出错转换处理,用于将出错转换为json或text出错响应,建议在业务逻辑中使用自定义的出错类型,使用出错中间件将相应的出错信息转换输出,可方便的汇总统计非自定义的出错类型,便于系统的优化。

Example

package main

import (
	"errors"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()
	e.Use(middleware.NewDefaultError())

	e.GET("/", func(c *elton.Context) (err error) {
		err = errors.New("abcd")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

etag

根据响应数据生成HTTP响应头的ETag,需要从BodyBuffer中生成,因此需要先通过Responder中间件将响应转换为Buffer或直接设置BodyBuffer。

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()
	e.Use(middleware.NewDefaultETag())

	e.GET("/", func(c *elton.Context) (err error) {
		c.BodyBuffer = bytes.NewBufferString("abcd")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

fresh

根据HTTP请求头与响应头判断是否未修改(304 Not Modified)。

Example

package main

import (
	"bytes"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {

	e := elton.New()
	e.Use(middleware.NewDefaultFresh())
	e.Use(middleware.NewDefaultETag())

	e.GET("/", func(c *elton.Context) (err error) {
		c.BodyBuffer = bytes.NewBufferString("abcd")
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

logger

Logger中间件,支持从请求头、响应头等获取信息,日志中标签以{}标记,支持的标签如下:

  • host 请求的host

  • method 请求的method

  • path 请求的path

  • proto 请求的协议类型

  • query 请求的raw query

  • remote 请求的remote addr

  • real-ip 客户的真实IP

  • client-ip 客户的IP,与real-ip的区别是会判断IP是否公网IP

  • scheme HTTP或者HTTPS

  • uri 请求的完整地址

  • referer 请求的referer

  • userAgent 请求的user agent

  • when 当前时间RFC1123带时区的格式化

  • when-iso 当前时间RFC3339的格式化

  • when-utc-iso 当前UTC时间的ISO格式化

  • when-unix 当前时间的unix时间戳(秒)

  • when-iso-ms 当前时间RFC3339的格式化(毫秒)

  • when-utc-iso-ms 当前UTC时间的ISO格式化(毫秒)

  • size 响应数据长度(字节)

  • size-human 响应数据长度,格式化为KB/MB(以1024换算)

  • status 状态码

  • latency 响应时间

  • latency-ms 响应时间(毫秒)

  • ~cookie 表示获取cookie的值,必须以~开头,后面的表示cookie的key

  • payload-size 提交数据长度(字节)

  • payload-size-human 提交数据长度,格式化为KB/MB(以1024换算)

  • >header 表示获取请求头的值,必须以>开头,后面表示header的key

  • <header 表示获取响应头的值,必须以<开头,后面表示header的key

  • :key 获取获取context设置的值,必须以:开头,后面表示对应的key,需要注意,设置至context中的值必须为string

  • $key 从ENV中获取该key对应的值,必须以$开头,后面表示对应的key

预定义了四种格式化模板(建议使用时自定义日志模板):

  • LoggerCombined: {remote} {when-iso} "{method} {uri} {proto}" {status} {size-human} "{referer}" "{userAgent}"

  • LoggerCommon: {remote} {when-iso} "{method} {uri} {proto}" {status} {size-human}

  • LoggerShort: {remote} {method} {uri} {proto} {status} {size-human} - {latency-ms} ms

  • LoggerTiny: {method} {url} {status} {size-human} - {latency-ms} ms

Example

package main

import (
	"fmt"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	// panic处理
	e.Use(middleware.NewRecover())
	e.Use(middleware.NewLogger(middleware.LoggerConfig{
		Format: middleware.LoggerCombined,
		OnLog: func(str string, _ *elton.Context) {
			fmt.Println(str)
		},
	}))

	// 响应数据转换为json
	e.Use(middleware.NewDefaultResponder())

	e.GET("/", func(c *elton.Context) error {
		c.Body = &struct {
			Message string `json:"message,omitempty"`
		}{
			"Hello, World!",
		}
		return nil
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

proxy

Proxy中间件,可以将指定的请求转发至另外的服务,并可重写url。

Example

package main

import (
	"net/url"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	target, _ := url.Parse("https://www.baidu.com")

	e.GET("/*", middleware.NewProxy(middleware.ProxyConfig{
		// proxy done will call this function
		Done: func(c *elton.Context) {

		},
		// http request url rewrite
		Rewrites: []string{
			"/api/*:/$1",
		},
		Target: target,
		// change the request host
		Host: "www.baidu.com",
	}))

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

recover

Recover中间件,用于捕获各种panic异常,避免程序异常退出,但建议自定义recover中间件,在获取到此类异常时,发送告警后做graceful restart。

Example

package main

import (
	"errors"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewRecover())

	e.GET("/", func(c *elton.Context) (err error) {
		panic(errors.New("abcd"))
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

renderer

模板渲染中间件,用于将各类模板渲染为html输出,默认支持htmltmpl两种后续文件使用html/template模块来渲染。

package main

import (
	"errors"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewRenderer(middleware.RendererConfig{}))

	e.GET("/", func(c *elton.Context) (err error) {
		c.Body = middleware.RenderData{
			File:         "index.html",
		}
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

responder

用于将Body转换为对应的字节数据,并设置响应头。默认的处理为将struct(map)转换为json,对于不同的应用可以指定Marshal与ContentType来实现自定义响应。

  • ResponderConfig.Marshal 自定义的Marshal函数,默认为json.Marshal

  • ResponderConfig.ContentType 自定义的ContentType,默认为application/json; charset=utf-8

Example

package main

import (
	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewDefaultResponder())

	// {"name":"tree.xie","id":123}
	e.GET("/", func(c *elton.Context) (err error) {
		c.Body = &struct {
			Name string `json:"name"`
			ID   int    `json:"id"`
		}{
			"tree.xie",
			123,
		}
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

response size limiter

响应长度限制中间件,可以限制响应数据的长度,避免返回过大的数据导致网络占用过大。此中间件主要用于避免一些非法调用等导致查询过多数据。

Example

package main

import (
	"bytes"
	"time"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewResponseSizeLimiter(middleware.ResponseSizeLimiterConfig{
		// 1MB
		MaxSize: 1024 * 1024,
	}))

	e.GET("/users/me", func(c *elton.Context) (err error) {
		time.Sleep(time.Second)
		c.BodyBuffer = bytes.NewBufferString(`{
			"account": "tree",
			"name": "tree.xie"
		}`)
		return nil
	})
	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

router concurrent limiter

路由限制中间件,可以指定路由的并发访问数量,建议使用NewLocalLimiter每个实例的限制分开,主要是用于避免某个接口并发过高导致系统不稳定。

Example

package main

import (
	"bytes"
	"time"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewRCL(middleware.RCLConfig{
		Limiter: middleware.NewLocalLimiter(map[string]uint32{
			"GET /users/me": 2,
		}),
	}))

	e.GET("/users/me", func(c *elton.Context) (err error) {
		time.Sleep(time.Second)
		c.BodyBuffer = bytes.NewBufferString(`{
			"account": "tree",
			"name": "tree.xie"
		}`)
		return nil
	})
	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

stats

HTTP请求的统计中间件,可以根据此中间件将http请求的各类统计信息写入至统计数据库,如:influxdb等,方便根据统计来优化性能以及监控。

Example

package main

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	e.Use(middleware.NewStats(middleware.StatsConfig{
		OnStats: func(info *middleware.StatsInfo, _ *elton.Context) {
			buf, _ := json.Marshal(info)
			fmt.Println(string(buf))
		},
	}))

	e.GET("/", func(c *elton.Context) (err error) {
		c.BodyBuffer = bytes.NewBufferString("abcd")
		return
	})
	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

static serve

静态文件处理中间件,默认支持通过目录访问,在实例使用中可以根据需求实现接口以使用各类不同的存储方式,如packr打包或mongodb存储等。

Example

package main

import (
	"time"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	sf := new(middleware.FS)
	// static file route
	e.GET("/*", middleware.NewStaticServe(sf, middleware.StaticServeConfig{
		Path: "/tmp",
		// 客户端缓存一年
		MaxAge: 365 * 24 * time.Hour,
		// 缓存服务器缓存一个小时
		SMaxAge:             time.Hour,
		DenyQueryString:     true,
		DisableLastModified: true,
		// 如果使用packr,它不支持Stat,因此需要用强ETag
		EnableStrongETag: true,
	}))

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

使用packr打包前端应用程序,通过static serve提供网站静态文件访问:

Example

package main

import (
	"bytes"
	"io"
	"time"
	"os"

	packr "github.com/gobuffalo/packr/v2"
	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

var (
	box = packr.New("asset", "./")
)

type (
	staticFile struct {
		box *packr.Box
	}
)

func (sf *staticFile) Exists(file string) bool {
	return sf.box.Has(file)
}
func (sf *staticFile) Get(file string) ([]byte, error) {
	return sf.box.Find(file)
}
func (sf *staticFile) Stat(file string) os.FileInfo {
	return nil
}
func (sf *staticFile) NewReader(file string) (io.Reader, error) {
	buf, err := sf.Get(file)
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(buf), nil
}

func main() {
	e := elton.New()

	sf := &staticFile{
		box: box,
	}

	// static file route
	e.GET("/static/*", middleware.NewStaticServe(sf, middleware.StaticServeConfig{
		// 客户端缓存一年
		MaxAge: 365 * 24 * time.Hour,
		// 缓存服务器缓存一个小时
		SMaxAge:             time.Hour,
		DenyQueryString:     true,
		DisableLastModified: true,
	}))

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

tracker

用于在客户提交类的请求添加跟踪日志,可输出query、body以及params等信息,并可设置正则匹配将关键数据加*处理。

Example

package main

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/vicanso/elton"
	"github.com/vicanso/elton/middleware"
)

func main() {
	e := elton.New()

	loginTracker := middleware.NewTracker(middleware.TrackerConfig{
		OnTrack: func(info *middleware.TrackerInfo, _ *elton.Context) {
			buf, _ := json.Marshal(info)
			fmt.Println(string(buf))
		},
	})

	e.Use(func(c *elton.Context) error {
		c.RequestBody = []byte(`{
			"account": "tree.xie",
			"password": "123456"
		}`)
		return c.Next()
	})

	e.POST("/user/login", loginTracker, func(c *elton.Context) (err error) {
		c.SetHeader(elton.HeaderContentType, elton.MIMEApplicationJSON)
		c.BodyBuffer = bytes.NewBuffer(c.RequestBody)
		return
	})

	err := e.ListenAndServe(":3000")
	if err != nil {
		panic(err)
	}
}

Last updated