48·Web 开发进阶

常用中间件实现

loggingauthcorsrecovery

第48课 - 常用中间件实现

学习目标

通过本课程的学习,你将能够:

  1. 理解 Web 中间件(Middleware)在 HTTP 请求处理链中的作用与基本原理。
  2. 熟练使用 Go 实现日志记录(Logging)和请求认证(Authentication)中间件。
  3. 掌握跨域资源共享(CORS)中间件的配置与实现。
  4. 了解并实现用于捕获 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)
}

实践练习

  1. 基础日志中间件:实现一个简单的日志中间件,仅打印请求的 HTTP 方法和 URL 路径,不记录耗时。并将其应用于一个输出 “Hello, World!” 的 Handler。

    • 预期:访问 http://localhost:8080/hello,服务器终端打印 GET /hello
  2. 扩展认证中间件:修改认证中间件,使其支持从 URL 查询参数 ?token=my-secret 中获取令牌进行验证(与请求头验证二选一即可)。

    • 要求:访问 http://localhost:8080/api?token=my-secret 应能成功;访问其他值或不带参数应返回 401。
  3. 中间件组合:将日志中间件(不记录耗时)、CORS 中间件(允许所有来源)和恢复中间件组合起来。然后创建一个会触发 panic 的 Handler(例如 panic(“出错了”))。

    • 要求:访问该 Handler 时,服务器应捕获 panic,打印日志,并返回 “服务器内部错误”,而不是崩溃。

常见错误

  1. 忘记调用 next.ServeHTTP:在中间件的前置逻辑完成后,忘记调用 next.ServeHTTP(w, r),导致请求处理链中断,后续中间件和业务 Handler 都不会执行。
  2. 在 Recovery 中间件之后执行可能 panic 的代码RecoveryMiddlewaredefer func() 必须在它所保护的函数栈顶。如果将其放在中间件链的最外层(如上例),则能捕获链内所有 Handler 的 panic。如果放错位置,可能无法捕获。
  3. 误用 return 终止请求:在认证失败等场景,使用 http.Error 后,必须使用 return 语句,否则代码会继续执行 next.ServeHTTP,这是严重的逻辑错误。
  4. CORS 中间件配置过于宽松:在生产环境中使用 Access-Control-Allow-Origin: * 可能带来安全风险。应配置为具体的可信域名。
  5. 中间件执行顺序混乱:例如,将认证中间件放在日志中间件外面,那么认证失败的请求就不会被日志记录,这可能不是期望的行为。顺序需要根据业务逻辑仔细设计。

小结

  • 中间件本质:是包装 http.Handler 的函数,用于在请求处理的前后插入通用逻辑,实现关注点分离。
  • 常用中间件
    • Logging:记录请求信息,便于监控和调试。
    • Authentication:验证请求身份,保护资源。
    • CORS:处理浏览器同源策略限制,使前后端分离架构可行。
    • Recovery:增强应用稳定性,优雅地处理运行时 panic。
  • 组合与顺序:通过函数嵌套(或使用第三方库的 Use 方法)可以将多个中间件组合成链,执行顺序至关重要,通常从外到内(如 Recovery -> CORS -> Logging -> Auth -> 业务)。
  • 核心价值:中间件模式使得横切关注点(cross-cutting concerns)可以被复用和组合,让业务 Handler 更加纯粹和简洁。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「Session 与 Cookie」 以巩固所学知识。