中间件
介绍
中间件是具有单个处理器的组件,在控制器处理器之前和/或之后执行。它们是过滤、拦截或更改进入应用程序的 HTTP 请求的便捷方式。
例如,中间件可用于认证用户。如果用户未认证,甚至在到达控制器处理器之前就会向用户发送消息。但是,如果用户已认证,中间件将传递给下一个处理器。这样的中间件称为阻塞式中间件。它可能会也可能不会传递给下一个处理器,下一个处理器可能是另一个中间件或控制器处理器。因此,阻塞式中间件充当一种条件门。
中间件还可用于清理用户输入,例如修剪字符串,将所有请求记录到日志文件中,自动向所有响应添加标头等。
编写中间件
每个中间件都在 http/middleware
包中的自己的文件中编写。
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()
。这样,中间件将在到达控制器处理器之前立即停止请求并响应。在以下示例中,假设您开发了一个自定义认证系统:
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)
}
}
内置中间件
框架提供了一些可选的中间件。
解析
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
。如果 ParseMultipartForm
或 ParseForm
返回错误,则返回 400 Bad request
。
在 multipart/form-data
中,所有文件部分都会自动转换为 []fsutil.File
。因此,在 request.Data
中,类型为 "file" 的字段将始终是 []fsutil.File
类型。它是一个切片,因此支持在单个字段中上传多文件。
如果匹配的路由是 "not found" 或 "method not allowed" 路由,则跳过中间件。
压缩
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
标头和 Encoder
的 Encoding()
方法返回的值选择。标头中的质量值会被考虑在内。
如果标头的值是 *
,则使用切片的第一个元素。如果接受的编码在 Encoders
切片中不可用,则响应不会被压缩,中间件立即传递。
如果中间件成功替换了响应写入器,则从请求中移除 Accept-Encoding
标头,以避免与潜在的其他编码中间件发生冲突。
如果在第一次调用 Write()
时未设置,中间件将使用 http.DetectContentType()
自动检测并设置 Content-Type
标头。
中间件会忽略被劫持的响应或包含 Upgrade
标头的请求。
自定义编码器
如果您需要支持更多压缩方法,可以轻松实现编码器。创建一个实现 compress.Encoder
接口的新结构。
例如,gzip 编码器的实现如下:
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
}
访问日志
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)
)来创建自己的访问日志格式化器:
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}
}
import "goyave.dev/goyave/v5/log"
router.GlobalMiddleware(&log.AccessMiddleware{Formatter: CustomFormatter})