第48课 - 常用中间件实现
学习目标
通过本课程的学习,你将能够:
- 理解 Web 中间件(Middleware)在 HTTP 请求处理链中的作用与基本原理。
- 熟练使用 Go 实现日志记录(Logging)和请求认证(Authentication)中间件。
- 掌握跨域资源共享(CORS)中间件的配置与实现。
- 了解并实现用于捕获 panic 的恢复(Recovery)中间件,增强应用的健壮性。
核心概念
什么是中间件? 想象一下,一个 HTTP 请求到达你的服务器,就像一位客人来到你家。在客人见到主人(你的业务处理函数)之前,可能会在门卫那里登记(日志)、检查身份(认证)、或者被告知是否可以带东西进门(CORS)。这些“门卫”或“安检”就是中间件。它们在请求到达最终的业务逻辑之前和之后,执行一些通用的、跨功能的处理。
在 Go 的 net/http 标准库中,我们通过包装 http.Handler 来实现中间件。一个典型的中间件函数签名是 func(next http.Handler) http.Handler,它接受下一个处理器作为参数,并返回一个新的处理器。
核心处理链(洋葱模型):
请求 -> 中间件1 (前置逻辑) -> 中间件2 (前置逻辑) -> 业务Handler -> 中间件2 (后置逻辑) -> 中间件1 (后置逻辑) -> 响应
代码示例
1. 日志记录中间件(Logging Middleware)
记录每个请求的方法、路径和耗时。
package main
import (
"log"
"net/http"
"time"
)
// LoggingMiddleware 是一个日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
// 返回一个新的 Handler,包装了 `next`
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 记录请求开始
start := time.Now()
log.Printf("开始处理请求: %s %s", r.Method, r.URL.Path)
// 调用下一个处理器,这可以是业务 Handler 或下一个中间件
next.ServeHTTP(w, r)
// 请求处理完成后,记录耗时
log.Printf("请求处理完成: %s %s,耗时: %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 一个简单的业务 Handler
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("欢迎访问首页!"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
// 将日志中间件包裹在最终的 mux 外面
loggedMux := LoggingMiddleware(mux)
log.Println("服务器启动在 :8080 端口...")
http.ListenAndServe(":8080", loggedMux)
}
运行效果:访问 http://localhost:8080/,终端会输出类似 2023/10/27 10:00:00 开始处理请求: GET / 和 2023/10/27 10:00:00 请求处理完成: GET /,耗时: 123.456µs。
2. 简易认证中间件(Authentication Middleware)
检查请求头中是否包含有效的 Authorization 令牌。
// AuthMiddleware 是一个简易的认证中间件
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从请求头中获取 Authorization 字段
token := r.Header.Get("Authorization")
// 这里进行简单的令牌校验(示例:固定令牌)
if token != "Bearer my-secret-token-123" {
// 认证失败,返回 401 未授权
http.Error(w, "未授权:请提供有效的访问令牌", http.StatusUnauthorized)
return // 直接返回,不调用 next
}
// 认证成功,继续执行下一个处理器
next.ServeHTTP(w, r)
})
}
3. 跨域资源共享中间件(CORS Middleware)
允许前端 JavaScript 应用(来自不同域名)访问后端 API。
import "strings"
// CORSMiddleware 处理 CORS 相关的响应头
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置允许的来源(生产环境应配置具体域名)
origin := r.Header.Get("Origin")
if origin != "" {
// 允许所有来源(开发环境)。生产环境请替换为具体域名检查。
w.Header().Set("Access-Control-Allow-Origin", "*")
// 允许的请求方法
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
// 允许的请求头
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
}
// 如果是预检请求(OPTIONS),直接返回成功
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
4. 恢复中间件(Recovery Middleware)
捕获业务逻辑中未处理的 panic,防止整个进程崩溃,并返回 500 错误。
// RecoveryMiddleware 捕获 panic 并返回 500 错误
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录错误详情(这里简化为打印,生产环境应使用日志库记录堆栈)
log.Printf("捕获到 panic: %v", err)
// 返回通用的 500 错误响应
http.Error(w, "服务器内部错误", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
5. 组合使用中间件
将多个中间件组合起来,形成一个处理链。
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
// 假设有一个需要认证的API
mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("这是受保护的数据"))
})
// 创建中间件链:Recovery -> CORS -> Logging -> Auth -> 最终路由
handler := RecoveryMiddleware(
CORSMiddleware(
LoggingMiddleware(
AuthMiddleware(mux), // 注意:认证中间件应该包裹需要保护的路由,或者在这里对所有路由进行认证。
),
),
)
log.Println("服务器启动在 :8080 端口...")
http.ListenAndServe(":8080", handler)
}
实践练习
-
基础日志中间件:实现一个简单的日志中间件,仅打印请求的 HTTP 方法和 URL 路径,不记录耗时。并将其应用于一个输出 “Hello, World!” 的 Handler。
- 预期:访问
http://localhost:8080/hello,服务器终端打印GET /hello。
- 预期:访问
-
扩展认证中间件:修改认证中间件,使其支持从 URL 查询参数
?token=my-secret中获取令牌进行验证(与请求头验证二选一即可)。- 要求:访问
http://localhost:8080/api?token=my-secret应能成功;访问其他值或不带参数应返回 401。
- 要求:访问
-
中间件组合:将日志中间件(不记录耗时)、CORS 中间件(允许所有来源)和恢复中间件组合起来。然后创建一个会触发 panic 的 Handler(例如
panic(“出错了”))。- 要求:访问该 Handler 时,服务器应捕获 panic,打印日志,并返回 “服务器内部错误”,而不是崩溃。
常见错误
- 忘记调用
next.ServeHTTP:在中间件的前置逻辑完成后,忘记调用next.ServeHTTP(w, r),导致请求处理链中断,后续中间件和业务 Handler 都不会执行。 - 在 Recovery 中间件之后执行可能 panic 的代码:
RecoveryMiddleware的defer func()必须在它所保护的函数栈顶。如果将其放在中间件链的最外层(如上例),则能捕获链内所有 Handler 的 panic。如果放错位置,可能无法捕获。 - 误用
return终止请求:在认证失败等场景,使用http.Error后,必须使用return语句,否则代码会继续执行next.ServeHTTP,这是严重的逻辑错误。 - CORS 中间件配置过于宽松:在生产环境中使用
Access-Control-Allow-Origin: *可能带来安全风险。应配置为具体的可信域名。 - 中间件执行顺序混乱:例如,将认证中间件放在日志中间件外面,那么认证失败的请求就不会被日志记录,这可能不是期望的行为。顺序需要根据业务逻辑仔细设计。
小结
- 中间件本质:是包装
http.Handler的函数,用于在请求处理的前后插入通用逻辑,实现关注点分离。 - 常用中间件:
- Logging:记录请求信息,便于监控和调试。
- Authentication:验证请求身份,保护资源。
- CORS:处理浏览器同源策略限制,使前后端分离架构可行。
- Recovery:增强应用稳定性,优雅地处理运行时 panic。
- 组合与顺序:通过函数嵌套(或使用第三方库的
Use方法)可以将多个中间件组合成链,执行顺序至关重要,通常从外到内(如 Recovery -> CORS -> Logging -> Auth -> 业务)。 - 核心价值:中间件模式使得横切关注点(cross-cutting concerns)可以被复用和组合,让业务 Handler 更加纯粹和简洁。