Skip to content

中间件

介绍

中间件是具有单个处理器组件,在控制器处理器之前和/或之后执行。它们是过滤、拦截或更改进入应用程序的 HTTP 请求的便捷方式。

例如,中间件可用于认证用户。如果用户未认证,甚至在到达控制器处理器之前就会向用户发送消息。但是,如果用户已认证,中间件将传递给下一个处理器。这样的中间件称为阻塞式中间件。它可能会也可能不会传递给下一个处理器,下一个处理器可能是另一个中间件或控制器处理器。因此,阻塞式中间件充当一种条件门。

中间件还可用于清理用户输入,例如修剪字符串,将所有请求记录到日志文件中,自动向所有响应添加标头等。

编写中间件

每个中间件都在 http/middleware 包中的自己的文件中编写。

go
type MyMiddleware struct {
	goyave.Component
}

func (m *MyMiddleware) Handle(next goyave.Handler) goyave.Handler {
	return func(response *goyave.Response, request *goyave.Request) {
		// 在栈中下一个处理器之前做一些事情
		next(response, request)
		// 在控制器处理器返回后做一些事情
	}
}

如果您希望中间件是阻塞式的,不要调用 next()。这样,中间件将在到达控制器处理器之前立即停止请求并响应。在以下示例中,假设您开发了一个自定义认证系统:

go
type CustomAuth struct {
	goyave.Component
}

func (m *CustomAuth) Handle(next goyave.Handler) goyave.Handler {
	return func(response *goyave.Response, request *goyave.Request) {
		if !auth.Check(request) {
            response.Status(http.StatusUnauthorized)
            return
        }
		next(response, request)
	}
}

内置中间件

框架提供了一些可选的中间件。

解析

go
import "goyave.dev/goyave/v5/middleware/parse"

router.GlobalMiddleware(&parse.Middleware{
	MaxUploadSize: 10, // 单位 MiB(默认为配置中 "server.maxUploadSize" 的值)
})

此中间件读取并解析原始请求查询和正文。

首先,使用 Go 的标准 url.ParseQuery() 解析查询。展平(单值数组转换为非数组)后,结果放入请求的 Query 中。如果解析失败,则返回 400 Bad request

仅当设置了 Content-Type 标头时才读取正文。如果正文超过配置的最大上传大小(单位 MiB),则返回 413 Request Entity Too Large。如果内容类型是 application/json,中间件将尝试解组正文并将结果放入请求的 Data 中。如果失败,则返回 400 Bad request

此中间件会耗尽请求的正文读取器。之后您无法读取 request.Body() 来访问未解析的数据。

如果内容类型是其他值,则调用 Go 的标准 ParseMultipartForm。结果在展平后放入请求的 Data 中。如果表单不是多部分表单,则尝试 ParseForm。如果 ParseMultipartFormParseForm 返回错误,则返回 400 Bad request

multipart/form-data 中,所有文件部分都会自动转换为 []fsutil.File。因此,在 request.Data 中,类型为 "file" 的字段将始终是 []fsutil.File 类型。它是一个切片,因此支持在单个字段中上传多文件。

如果匹配的路由是 "not found" 或 "method not allowed" 路由,则跳过中间件。

压缩

go
import (
	"compress/gzip"

	"goyave.dev/goyave/v5/middleware/compress"
)

compress := &compress.Middleware{
	Encoders: []compress.Encoder{
		&compress.Gzip{
			Level: gzip.BestCompression,
		},
	},
}
router.Middleware(compress)

此中间件压缩 HTTP 响应,并由于 compress.Encoders 切片支持多种算法。

编码器将根据请求的 Accept-Encoding 标头和 EncoderEncoding() 方法返回的值选择。标头中的质量值会被考虑在内。

如果标头的值是 *,则使用切片的第一个元素。如果接受的编码在 Encoders 切片中不可用,则响应不会被压缩,中间件立即传递。

如果中间件成功替换了响应写入器,则从请求中移除 Accept-Encoding 标头,以避免与潜在的其他编码中间件发生冲突。

如果在第一次调用 Write() 时未设置,中间件将使用 http.DetectContentType() 自动检测并设置 Content-Type 标头。

中间件会忽略被劫持的响应或包含 Upgrade 标头的请求。

自定义编码器

如果您需要支持更多压缩方法,可以轻松实现编码器。创建一个实现 compress.Encoder 接口的新结构。

例如,gzip 编码器的实现如下:

go
import (
	"compress/gzip"

	"goyave.dev/goyave/v5/util/errors"
)

type Gzip struct {
	Level int
}

func (w *Gzip) Encoding() string {
	return "gzip"
}

func (w *Gzip) NewWriter(wr io.Writer) io.WriteCloser {
	writer, err := gzip.NewWriterLevel(wr, w.Level)
	if err != nil {
		panic(errors.New(err))
	}
	return writer
}

访问日志

go
import "goyave.dev/goyave/v5/log"

router.GlobalMiddleware(log.CombinedLogMiddleware())
// 或
router.GlobalMiddleware(log.CommonLogMiddleware())

要启用使用通用日志格式记录访问,只需注册 CommonLogMiddleware。或者,您可以使用 CombinedLogMiddleware 进行组合日志格式。

日志以结构化格式输出。消息本身按照标准定义格式化。每个信息片段(主机、时间、方法、uri、协议、状态、长度等)的属性都附加到日志条目中,位于名为 "details" 的属性组中。

如果启用了调试(配置 app.debug),则仅输出消息以避免混乱。

自定义格式化器

您可以通过实现类型为 log.Formatter 的函数(func(ctx *Context) (message string, attributes []slog.Attr))来创建自己的访问日志格式化器:

go
func CustomFormatter(ctx *log.Context) (string, []slog.Attr) {
	method := ctx.Request.Method()
	uri := ctx.Request.URL().RequestURI()
	message: = fmt.Sprintf("%s %s", method, uri)

	details := slog.Group("details",
		slog.String("method", req.Method),
		slog.String("uri", uri),
	)

	return message, []slog.Attr{details}
}
go
import "goyave.dev/goyave/v5/log"

router.GlobalMiddleware(&log.AccessMiddleware{Formatter: CustomFormatter})