go-zero(八) 中间件的使用

go zero 中间件的使用

一、中间件介绍

中间件(Middleware)是一个在请求和响应处理之间插入的程序或者函数,它可以用来处理、修改或者监控 HTTP 请求和响应的各个方面。中间件通常位于 web 服务器和应用逻辑之间,它们可以拦截请求和响应流,对其进行某种处理,然后决定是否将控制传递给下一个中间件或最终的处理程序。

1.中间件的核心概念

  1. 请求拦截:中间件能够在请求到达目标处理器之前,对请求进行分析、修改或记录。

  2. 响应拦截:同样,中间件也可以在响应返回给客户端之前,对响应进行处理,例如添加 headers、压缩响应体、格式化响应数据等。

  3. 链式调用:多个中间件可以串联在一起, 形成一个中间件链,依次处理请求和响应。

2.中间件的用途

中间件可以用于以下多个方面:

  1. 日志记录:记录 HTTP 请求和响应的详细信息,例如请求路径、方法、状态码等。这有助于进行审计和调试。
  2. 认证和授权:验证请求是否具有必要的权限,确保用户或系统的身份是否合法。这对于保护敏感接口尤其重要。
  3. 请求限流:控制访问速率,避免服务被恶意请求或流量洪水淹没。这可以提高系统的稳定性和可用性。
  4. 跨域资源共享 (CORS):处理浏览器的跨域请求。中间件可以在响应中添加适当的 CORS 头,允许来自不同源的请求。
  5. 错误处理:捕获处理过程中的错误(如 panic)并生成适当的 HTTP 响应。这样可以确保系统在错误情况下不会崩溃,并能够正常返回相应。
  6. 数据格式化:统一请求和响应的数据格式,例如将响应数据转换为 JSON 格式,或者对请求中的参数进行验证和转换。
  7. 国际化 (i18n):处理用户的语言偏好和内容的本地化,使应用能够支持多语言。

3.中间件的工作流程

一般来说,工作流程如下:

  1. 用户发起请求,请求到达 Web 服务器。
  2. 中间件链开始处理:
    • 第一个中间件接收请求,进行相应的处理(如日志记录)。
    • 控制权传递给下一个中间件。
    • 这个过程可以一直持续,直到所有中间件处理完请求。
  3. 最终请求到达目标处理器,处理器生成响应。
  4. 响应同样会经过中间件链进行处理(如添加 headers)。
  5. 最终响应返回给用户。

这种结构类似于"洋葱模型":请求像是从外到内穿过洋葱层,而响应则是从内到外穿出。

image.png

二、go zero内置中间件介绍

go zero 提供了一系列内置的中间件,以帮助开发者管理 HTTP 请求的不同方面。

还记得之前的jwt鉴权吗?那边我们只实现了token生成,但是并没有实现验证,但是go zero能够自动解析token,就是因为它内置了AuthorizeHandler(鉴权管理中间件)

1. 内置中间件一览

在 go zero 中内置了如下中间件:

中间件名称 功能描述 默认状态
AuthorizeHandler 身份验证与授权控制 启用
BreakerHandler 服务熔断,防止级联故障 启用
LogHandler 请求日志记录 启用
MetricHandler 服务指标统计 启用
PrometheusHandler Prometheus 监控指标收集 启用
RecoverHandler Panic 恢复,防止服务崩溃 启用
TimeoutHandler 请求超时控制 启用
TraceHandler 分布式链路追踪 启用
MaxConnsHandler 最大连接数限制 启用
SheddingHandler 负载均衡与请求分流 启用
GunzipHandler 响应压缩管理 启用
MaxBytesHandler 请求体大小限制 启用
ContentSecurityHandler 内容安全策略 启用
CryptionHandler 请求/响应加解密 启用

这些中间件的具体实现可以去看github.com\zeromicro\go zero@v1.7.3\rest\handler目录的内容。

2. 核心中间件详解

认证中间件 (AuthorizeHandler)

JWT 认证是 Go-Zero 中最常用的身份验证方式,由内置的 AuthorizeHandler 提供支持。

工作原理

  1. 验证请求头中的 Authorization 字段
  2. 解析 JWT Token 并验证有效性
  3. 将用户信息存入上下文,供后续处理器使用

源码解析

// Authorize 函数用于创建一个 HTTP 中间件,该中间件可对传入的 HTTP 请求进行身份验证。
// 它会从请求中解析 JWT(JSON Web Token),验证其有效性,并将 JWT 中的自定义声明添加到请求的上下文(context)中,供后续处理程序使用。
// 参数 secret 是用于验证 JWT 的密钥,为字符串类型。
// 参数 opts 是可变参数,类型为 AuthorizeOption,用于配置身份验证的选项。
// 函数返回一个 func(http.Handler) http.Handler 类型的函数,即一个 HTTP 中间件。
func Authorize(secret string, opts ...AuthorizeOption) func(http.Handler) http.Handler {
    // 定义一个 AuthorizeOptions 类型的变量 authOpts,用于存储身份验证的选项。
    var authOpts AuthorizeOptions
    // 遍历传入的选项 opts
    for _, opt := range opts {
        // 调用每个选项函数,将 authOpts 的指针传递给它们,以设置具体的选项。
        opt(&authOpts)
    }

    // 创建一个 token.TokenParser 类型的实例 parser,用于解析 JWT。
    parser := token.NewTokenParser()

    // 返回一个中间件函数,该函数接受一个 http.Handler 类型的参数 next,表示下一个处理程序。
    return func(next http.Handler) http.Handler {
        // 返回一个 http.HandlerFunc 类型的处理程序,用于处理传入的 HTTP 请求。
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 调用 parser 的 ParseToken 方法,从请求 r 中解析 JWT,同时传入当前密钥 secret 和之前的密钥 authOpts.PrevSecret。
            // tok 是解析出的 JWT 对象,err 是可能出现的错误。
            tok, err := parser.ParseToken(r, secret, authOpts.PrevSecret)
            if err != nil {
                // 如果解析过程中出现错误,说明 token 无效或解析失败。
                // 调用 unauthorized 函数返回未授权错误,并终止请求处理。
                unauthorized(w, r, err, authOpts.Callback)
                return
            }

            // 检查解析出的 JWT 是否有效。
            if !tok.Valid {
                // 如果 JWT 无效,调用 unauthorized 函数返回未授权错误,并终止请求处理。
                unauthorized(w, r, errInvalidToken, authOpts.Callback)
                return
            }

            // 尝试将 JWT 的声明(Claims)转换为 jwt.MapClaims 类型。
            // claims 是转换后的声明对象,ok 表示转换是否成功。
            claims, ok := tok.Claims.(jwt.MapClaims)
            if !ok {
                // 如果转换失败,说明 JWT 中没有有效的声明。
                // 调用 unauthorized 函数返回未授权错误,并终止请求处理。
                // errNoClaims 是一个预定义的错误对象,表示没有声明。
                unauthorized(w, r, errNoClaims, authOpts.Callback)
                return
            }

            // 获取请求的上下文 ctx。
            ctx := r.Context()
            // 遍历 JWT 的声明,将自定义声明添加到上下文中。
            for k, v := range claims {
                switch k {
                // 忽略标准的 JWT 声明,如受众、过期时间、ID 等。
                case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
                    // 忽略标准的声明,不做处理
                default:
                    // 将自定义声明添加到上下文中,键为 k,值为 v。
                    ctx = context.WithValue(ctx, k, v)
                }
            }

            // 将带有更新后的上下文的请求传递给下一个处理程序 next 进行处理。
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

熔断中间件 (BreakerHandler)

熔断器是微服务架构中保障系统稳定性的关键组件,它能在服务出现故障时快速熔断,防止故障扩散。

工作原理

  1. 监控 API 调用的成功/失败状态
  2. 当失败率达到阈值时触发熔断
  3. 熔断期间快速拒绝请求,避免对故障服务持续请求
  4. 熔断一段时间后进入半开状态,尝试恢复服务

源码解析

// BreakerHandler 返回一个熔断中间件。熔断机制用于在系统出现故障或过载时,暂时切断对特定服务的访问,避免系统进一步恶化。
// 参数 method 表示 HTTP 请求的方法(如 GET、POST 等),path 表示请求的路径,metrics 用于统计请求的指标信息。
func BreakerHandler(method, path string, metrics *stat.Metrics) func(http.Handler) http.Handler {
    // 创建一个新的熔断实例 brk。使用 method 和 path 组合作为熔断实例的名称,便于区分不同的路由。
    // breakerSeparator 是一个分隔符,用于连接 method 和 path 形成唯一的名称。
    brk := breaker.NewBreaker(breaker.WithName(strings.Join([]string{method, path}, breakerSeparator)))
    // 返回一个函数,该函数接受一个 http.Handler 类型的参数 next,表示下一个处理程序。
    return func(next http.Handler) http.Handler {
        // 返回一个 http.HandlerFunc 类型的处理程序,用于处理传入的 HTTP 请求。
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 尝试从熔断实例 brk 中获取执行许可,返回一个 promise 和可能的错误。
            // 如果熔断打开(即不允许执行),则会返回错误。
            promise, err := brk.Allow()
            if err != nil {
                // 如果获取许可失败,说明熔断已经打开,需要进行相应处理。
                // 增加统计指标中的丢弃请求数量。
                metrics.AddDrop()
                // 记录错误日志,包含请求的 URI、远程地址和用户代理信息。
                logc.Errorf(r.Context(), "[http] dropped, %s - %s - %s",
                    r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())
                // 返回 HTTP 503 状态码,表示服务不可用。
                w.WriteHeader(http.StatusServiceUnavailable)
                return
            }

            // 创建一个自定义的响应写入器 cw,用于记录响应的状态码。
            cw := response.NewWithCodeResponseWriter(w)
            // 使用 defer 关键字确保在函数返回时执行以下代码块。
            defer func() {
                // 根据响应的状态码判断请求是否成功。
                // 如果状态码小于 500(即非内部服务器错误),表示请求成功。
                if cw.Code < http.StatusInternalServerError {
                    // 调用 promise 的 Accept 方法,表示请求成功,熔断实例可以继续正常工作。
                    promise.Accept()
                } else {
                    // 如果状态码大于等于 500,表示请求失败。
                    // 调用 promise 的 Reject 方法,并传入错误信息,可能会触发熔断打开。
                    promise.Reject(fmt.Sprintf("%d %s", cw.Code, http.StatusText(cw.Code)))
                }
            }()
            // 将请求传递给下一个处理程序进行处理。
            next.ServeHTTP(cw, r)
        })
    }
}

链路追踪中间件 (TraceHandler)

在微服务架构中,一个用户请求可能涉及多个服务调用。链路追踪帮助开发者了解请求在服务间的流转路径。

工作原理

  1. 为每个请求生成唯一的 TraceID
  2. 在服务间传递 TraceID
  3. 记录请求在各个服务中的耗时
  4. 构建完整的调用链路图

源码解析


// TraceHandler 返回一个用于处理 OpenTelemetry 追踪的中间件。
// 该中间件会为每个符合条件的 HTTP 请求创建一个新的追踪跨度(span),并记录请求的相关信息。
// 参数 serviceName 表示服务的名称,用于在追踪信息中标识服务。
// 参数 path 表示请求路径,用于指定追踪跨度的名称。
// 参数 opts 是可变参数,用于配置追踪选项。
func TraceHandler(serviceName, path string, opts ...TraceOption) func(http.Handler) http.Handler {
    // 定义一个 traceOptions 类型的变量 options,用于存储追踪选项
    var options traceOptions
    // 遍历传入的选项 opts
    for _, opt := range opts {
        // 调用每个选项函数,将 options 的指针传递给它们,以设置具体的选项
        opt(&options)
    }

    // 创建一个集合 ignorePaths,用于存储需要忽略的请求路径
    ignorePaths := collection.NewSet()
    // 将 options.traceIgnorePaths 中的所有路径添加到集合中
    ignorePaths.AddStr(options.traceIgnorePaths...)

    // 返回一个中间件函数,该函数接受一个 http.Handler 类型的参数 next,表示下一个处理程序
    return func(next http.Handler) http.Handler {
        // 创建一个 OpenTelemetry 追踪器 tracer
        tracer := otel.Tracer(trace.TraceName)
        // 获取 OpenTelemetry 的文本映射传播器 propagator
        propagator := otel.GetTextMapPropagator()

        // 中间件函数返回一个 http.HandlerFunc 类型的处理程序,用于处理传入的 HTTP 请求
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 初始化跨度名称为传入的 path
            spanName := path
            // 如果 path 为空,则使用请求的 URL 路径作为跨度名称
            if len(spanName) == 0 {
                spanName = r.URL.Path
            }

            // 检查 spanName 是否在忽略路径集合中
            if ignorePaths.Contains(spanName) {
                // 如果在忽略路径集合中,则直接调用下一个处理程序,不进行追踪
                next.ServeHTTP(w, r)
                return
            }

            // 使用传播器从请求头中提取上下文信息
            ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
            // 使用追踪器启动一个新的跨度
            // spanCtx 是新创建的跨度上下文,span 是新创建的跨度对象
            // oteltrace.WithSpanKind(oteltrace.SpanKindServer) 设置跨度类型为服务器端跨度
            // oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serviceName, spanName, r)...) 添加 HTTP 请求的相关属性
            spanCtx, span := tracer.Start(
                ctx,
                spanName,
                oteltrace.WithSpanKind(oteltrace.SpanKindServer),
                oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(
                    serviceName, spanName, r)...),
            )
            // 使用 defer 关键字确保在函数返回时结束跨度
            defer span.End()

            // 使用传播器将跨度上下文信息注入到响应头中,方便后续追踪
            propagator.Inject(spanCtx, propagation.HeaderCarrier(w.Header()))

            // 创建一个自定义的响应写入器 trw,用于记录响应的状态码
            trw := response.NewWithCodeResponseWriter(w)
            // 将带有更新后的上下文的请求传递给下一个处理程序进行处理
            next.ServeHTTP(trw, r.WithContext(spanCtx))

            // 根据响应的状态码设置跨度的属性
            span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(trw.Code)...)
            // 根据响应的状态码和跨度类型设置跨度的状态
            span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(
                trw.Code, oteltrace.SpanKindServer))
        })
    }
}

3. 中间件配置与管理

以上中间件都是默认启用,go-zero 允许我们通过配置文件灵活控制中间件的启用/禁用状态:

如果你看过RestConf 的代码,就应该能看到,它里面包含了一个Middlewares 中间件的配置结构。

    RestConf struct {
        service.ServiceConf
        Host     string `json:",default=0.0.0.0"`
        Port     int
        /*
        ....
        */
        //中间件配置相关的结构体
        Middlewares MiddlewaresConf
        // TraceIgnorePaths is paths blacklist for trace middleware.
    
    }

我们可以看下它的具体内容,可以看到这个结构体主要用来控制,这些中间件是否启动,默认都是开启状态:

MiddlewaresConf struct {
        Trace      bool `json:",default=true"`
        Log        bool `json:",default=true"`
        Prometheus bool `json:",default=true"`
        MaxConns   bool `json:",default=true"`
        Breaker    bool `json:",default=true"`
        Shedding   bool `json:",default=true"`
        Timeout    bool `json:",default=true"`
        Recover    bool `json:",default=true"`
        Metrics    bool `json:",default=true"`
        MaxBytes   bool `json:",default=true"`
        Gunzip     bool `json:",default=true"`
    }

那么如何控制这些中间件是否关闭就很简单了,例如我想关闭Prometheus中间件,打开yaml文件:

Middlewares:
  Prometheus: false #把值设置为false

三、自定义中间件

尽管go-zero 提供了丰富的内置中间件,但在实际项目中,我们经常需要开发自定义中间件来满足特定业务需求。

1.局部中间件

局部中间件仅应用于特定路由组或服务块,是实现业务逻辑分离的有效手段。

步骤 1: 在 API 文件中声明中间件

在 go zero 中,我们通过 api 语言来声明 HTTP 服务,然后通过 goctl 生成 HTTP 服务代码。

type (
    RegisterRequest {
        //请求体定义了 Username 和Password 字段, 并且都设置了不能为空
        Username string `json:"username" validate:"required"`
        Password string `json:"password" validate:"required"`
    }
    RegisterResponse {
        //响应体 定义类一个Message  用来返回结果
        Message string `json:"message"`
    }

    LoginRequest {
        Username string `json:"username" validate:"required"`
        Password string `json:"password" validate:"required"`
    }
    LoginResponse {
        Token string `json:"token"`
    }
)

@server (
    group:      user // 代表当前 service 代码块下的路由生成代码时都会被放到 user 目录下
    prefix:     /v1 //定义路由前缀为 "/v1"
    middleware: TestMiddleware    
)

service user {
    @handler Register
    post /register (RegisterRequest) returns (RegisterResponse)
    
    @handler Login
    post /login (LoginRequest) returns (LoginResponse)
}

在上面的例子中,我们声明了一个中间件TestMiddleware,然后在 @server 中通过 middileware 关键字来声明中间件。

  middleware: UserRateLimiter, RequestValidator  // 声明多个中间件

也可以这样声明多个中间件

==需要说明的,我们这个中间件是局部中间件,仅对当前的server有效 #EE3F4D==

使用以下命令更新代码:

goctl api go --api user.api --dir .

命令执行完后,会在项目中生成middleware文件夹,以及中间件相关代码

我们可以先看下routes.go文件,可以看到这路由前面帮我加了一个中间件

image.png

步骤 2: 实现中间件逻辑

接下来我们打开middleware目录下的文件,我们简单的添加一个header信息,修改代码:

func (m *TestMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO generate middleware implement function, delete after code implementation
        //我们自定义一个header信息
        w.Header().Set("xxxx", "aaaabb")
        next(w, r)
    }
}

步骤 3: 在 ServiceContext 中注册中间件

然后把中间件注册到服务中,打开servicecontext.go文件:

type ServiceContext struct {
    Config                config.Config
    UserModel             model.UsersModel
    UserRpc               user.User
    TestMiddleware rest.Middleware  //定义中间件
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:                c,
        UserModel:             model.NewUsersModel(sqlx.NewMysql(c.MysqlDB.DbSource)),
        UserRpc:               user.NewUser(zrpc.MustNewClient(c.UserRpcConf)),
        //初始化中间件
        TestMiddleware Middleware: middleware.NewTestMiddleware Middleware().Handle,
    }
}

运行项目测试

image.png

2.进阶实现限流中间

现在来实现一个局部的限流中间件 ,更好的理解中间件。

步骤 1: 在 API 文件中声明中间件


@server (
    group:      user // 代表当前 service 代码块下的路由生成代码时都会被放到 user 目录下
    prefix:     /v1 //定义路由前缀为 "/v1"
    middleware: UserRateLimiter //更换成限流中间件
)
service user {
    @handler Register
    post /register (RegisterRequest) returns (RegisterResponse)

    @handler Login
    post /login (LoginRequest) returns (LoginResponse)
}


使用以下命令更新代码:

goctl api go --api user.api --dir .

步骤 2: 实现中间件逻辑

修改internal/middleware/userratelimitermiddleware.go文件:


type UserRateLimiterMiddleware struct {
    // 以用户 IP 为 key,记录请求时间
    requestRecords map[string][]time.Time
    mutex          sync.Mutex
    // 时间窗口内允许的最大请求次数
    maxRequests int
    // 时间窗口大小 (秒)
    windowSize time.Duration
}

func NewUserRateLimiterMiddleware() *UserRateLimiterMiddleware {
    return &UserRateLimiterMiddleware{
        requestRecords: make(map[string][]time.Time),
        maxRequests:    1,   //为了方便演示,把最大请求改为1
        windowSize:     time.Second * 10, //窗口时间设置为10秒
    }
}

func (m *UserRateLimiterMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // TODO generate middleware implement function, delete after code implementation
        // 获取客户端 IP
        clientIP := r.RemoteAddr
        fmt.Println(clientIP)
        m.mutex.Lock()
        defer m.mutex.Unlock()

        now := time.Now()

        // 清理过期的请求记录
        if records, exists := m.requestRecords[clientIP]; exists {
            var validRecords []time.Time
            for _, t := range records {
                if now.Sub(t) <= m.windowSize {
                    validRecords = append(validRecords, t)
                }
            }
            m.requestRecords[clientIP] = validRecords
        }

        // 检查请求频率
        if len(m.requestRecords[clientIP]) >= m.maxRequests {
            logx.Infof("IP %s 请求频率过高", clientIP)

            http.Error(w, "请求过于频繁,请稍后再试", http.StatusTooManyRequests)

        }

        // 记录本次请求
        m.requestRecords[clientIP] = append(m.requestRecords[clientIP], now)

        // 继续处理请求
        next(w, r)
    }
}

步骤 3: 实现登录逻辑

修改loginlogic.go ,实现简单的登录逻辑,用来测试

func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
    // todo: add your logic here and delete this line
    if req.Username == "admin" && req.Password == "123456" {
        return &types.LoginResponse{
            Token: "admin-token",
        }, nil
    }
    return
}


步骤 4: 测试中间件

运行项目,不断的点击请求, 发现中间件启用

image.png

3. 全局中间件

全局中间件适用于所有路由,常用于实现通用功能,如 CORS 支持、Gzip 压缩等。

步骤1:实现请求计时中间件

我们通过实现一个请求计时中间件来演示

package middleware

import (
    "net/http"
    "time"
    
    "github.com/zeromicro/go-zero/core/logx"
)
// middleware/timer.go
func RequestTimer(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // 调用下一个处理函数
        next(w, r)
        
        // 计算请求处理时间
        elapsed := time.Since(start)
        logx.Infof("请求: %s %s 处理时间: %v", r.Method, r.URL.Path, elapsed)
    }
}

步骤2:注册全局中间件

然后将其注册到 go zero 的 rest 中

func main() {
    flag.Parse()

    var c config.Config
    conf.MustLoad(*configFile, &c)

    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    server.Use(middleware.RequestTimer) // 注册请求计时器中间件

    ctx := svc.NewServiceContext(c)
    handler.RegisterHandlers(server, ctx)

    fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
    server.Start()
}

步骤3:测试中间件

image.png

四、中间件最佳实践

遵循以下原则可以帮助你设计出高质量的中间件:

  • 单一职责:每个中间件只负责一项特定功能
  • 高内聚低耦合:减少中间件间的相互依赖
  • 高性能:避免在中间件中执行耗时操作
  • 可配置性:通过配置控制中间件行为
  • 幂等性:多次应用同一中间件不会产生副作用
  • 透明性:中间件不应改变请求和响应的语义

2. 中间件顺序与性能

中间件的执行顺序对于系统行为和性能有显著影响:

客户端 --> [日志] --> [限流] --> [认证] --> 业务逻辑 --> [压缩] --> [CORS] --> 客户端

建议的中间件执行顺序:

  1. 记录与监控:日志、链路追踪等
  2. 安全防护:限流、熔断等
  3. 身份验证:认证、授权等
  4. 请求处理:请求解析、参数验证等
  5. 响应处理:响应压缩、CORS 等
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容