中间件
介绍
中间件是具有单个处理器的组件,在控制器处理器之前和/或之后执行。它们是过滤、拦截或更改进入应用程序的 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})