Skip to content

中间件与拦截器

本篇讲解 go-zero 的横切关注点处理机制:API 中间件(Middleware)和 RPC 拦截器(Interceptor)。我们将介绍它们的注册方式、执行顺序,并实现常用的中间件:JWT 认证、请求日志、限流、CORS。最后对比中间件与拦截器的差异,并通过一个完整示例把 API + RPC 的认证与限流串起来。

一、API 中间件

go-zero 的 rest 包支持中间件机制,与 Gin/Echo 类似,但注册方式略有不同。中间件签名:

go
type Middleware func(next http.HandlerFunc) http.HandlerFunc

即:接收下一个 handler,返回一个新的 handler。中间件可以在请求前后插入逻辑。

1. 全局中间件

全局中间件对所有路由生效,在 main.go 中通过 server.Use 注册:

go
func main() {
    // ...
    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()

    // 全局中间件(所有路由生效)
    server.Use(middleware.NewLogMiddleware().Handle)
    server.Use(rest.ToMiddleware(func(next http.HandlerFunc) http.HandlerFunc {
        return func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("X-Trace-Id", traceIDFromContext(r.Context()))
            next(w, r)
        }
    }))

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

rest.ToMiddleware 把一个 func(http.HandlerFunc) http.HandlerFunc 转成 rest.Middleware,方便快速写简单的中间件。

2. 路由组中间件

更常见的场景是「某组路由才需要某中间件」。go-zero 通过 .api 文件的 @server 块声明:

api
@server (
    group: user
    prefix: /api/v1
    jwt: Auth
    middleware: LogMiddleware,RateLimitMiddleware
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)
}

goctl 会生成对应的中间件调用代码。但中间件的实现需要在 ServiceContext 中提供:

go
// internal/svc/servicecontext.go
type ServiceContext struct {
    Config              config.Config
    LogMiddleware       rest.Middleware
    RateLimitMiddleware rest.Middleware
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:              c,
        LogMiddleware:       middleware.NewLogMiddleware().Handle,
        RateLimitMiddleware: middleware.NewRateLimitMiddleware(c.RateLimit).Handle,
    }
}

注意:@servermiddleware 写的是字段名(去掉包名前缀,且首字母小写转大写后的形式),goctl 会从 ServiceContext 取同名字段。

3. 自定义中间件

自定义中间件放在 internal/middleware/ 目录,每个中间件一个文件:

go
// internal/middleware/logmiddleware.go
package middleware

import (
    "fmt"
    "net/http"
    "time"

    "github.com/zeromicro/go-zero/core/logx"
)

type LogMiddleware struct {
}

func NewLogMiddleware() *LogMiddleware {
    return &LogMiddleware{}
}

func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        // 用自定义 ResponseWriter 捕获状态码
        rw := &statusRecorder{ResponseWriter: w, statusCode: http.StatusOK}
        next(rw, r)
        cost := time.Since(start).Milliseconds()
        logx.Infof("[HTTP] %s %s %d %dms %s",
            r.Method, r.URL.Path, rw.statusCode, cost, r.RemoteAddr)
    }
}

type statusRecorder struct {
    http.ResponseWriter
    statusCode int
}

func (r *statusRecorder) WriteHeader(code int) {
    r.statusCode = code
    r.ResponseWriter.WriteHeader(code)
}

4. 中间件执行顺序

go-zero 中间件的执行顺序遵循「洋葱模型」:

全局中间件1 → 全局中间件2 → 路由组中间件1 → 路由组中间件2 → Handler

全局中间件1 ← 全局中间件2 ← 路由组中间件1 ← 路由组中间件2 ← Handler

即:先注册的中间件在外层,后注册的在内层。请求进入时从外到内执行 next 之前的逻辑,响应返回时从内到外执行 next 之后的逻辑。

二、常用中间件实现

1. JWT 认证中间件

go-zero 内置了 JWT 支持,只需要在 .api 文件的 @server 中声明 jwt: Auth,框架会自动校验。但有时我们需要自定义 JWT 逻辑(如刷新 token、多端登录),可以自己实现。

内置 JWT 用法

.api 文件:

api
@server (
    group: user
    prefix: /api/v1
    jwt: Auth
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)
}

配置:

yaml
Auth:
  AccessSecret: "your-256-bit-secret"
  AccessExpire: 86400

生成 token 的 Logic:

go
// internal/logic/auth/loginlogic.go
package auth

import (
    "context"
    "fmt"
    "time"

    "user-api/internal/svc"
    "user-api/internal/types"

    "github.com/golang-jwt/jwt/v5"
    "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{
        Logger: logx.WithContext(ctx),
        ctx:    ctx,
        svcCtx: svcCtx,
    }
}

func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
    // 1. 校验账号密码(简化示例)
    if req.Username != "admin" || req.Password != "123456" {
        return nil, fmt.Errorf("invalid username or password")
    }

    // 2. 生成 JWT token
    now := time.Now().Unix()
    expire := l.svcCtx.Config.Auth.AccessExpire
    claims := jwt.MapClaims{
        "userId": 1,
        "username": req.Username,
        "exp":      now + expire,
        "iat":      now,
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(l.svcCtx.Config.Auth.AccessSecret))
    if err != nil {
        return nil, fmt.Errorf("sign token failed: %v", err)
    }

    return &types.LoginResponse{
        Token:      tokenString,
        ExpireAt:   now + expire,
    }, nil
}

框架会在请求进入 jwt: Auth 路由组时自动校验 token,并把 claims 注入到 context.Context 中。在 Logic 中可以这样取出用户 ID:

go
import "github.com/zeromicro/go-zero/core/logx"

func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
    userId, _ := l.ctx.Value("userId").(json.Number).Int64()
    // 或者用 logx 拿 traceID 风格的方式
    // ...
}

自定义 JWT 中间件

如果内置 JWT 不满足需求(如需要从 Redis 检查 token 是否被吊销),可以自定义:

go
// internal/middleware/jwtmiddleware.go
package middleware

import (
    "context"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
)

type JWTMiddleware struct {
    Secret string
}

func NewJWTMiddleware(secret string) *JWTMiddleware {
    return &JWTMiddleware{Secret: secret}
}

func (m *JWTMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "missing authorization header", http.StatusUnauthorized)
            return
        }
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "invalid authorization header", http.StatusUnauthorized)
            return
        }
        tokenStr := parts[1]
        claims := jwt.MapClaims{}
        token, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (any, error) {
            return []byte(m.Secret), nil
        })
        if err != nil || !token.Valid {
            http.Error(w, "invalid token", http.StatusUnauthorized)
            return
        }
        // 把 claims 放入 context
        ctx := context.WithValue(r.Context(), "claims", claims)
        next(w, r.WithContext(ctx))
    }
}

2. 请求日志中间件

go
// internal/middleware/logmiddleware.go
package middleware

import (
    "net/http"
    "time"

    "github.com/zeromicro/go-zero/core/logx"
)

type LogMiddleware struct{}

func NewLogMiddleware() *LogMiddleware { return &LogMiddleware{} }

func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        recorder := &bodyRecorder{ResponseWriter: w, status: http.StatusOK}
        next(recorder, r)
        cost := time.Since(start)
        logx.WithContext(r.Context()).Slowf("[HTTP] %s %s %d %s cost=%v",
            r.Method, r.URL.RequestURI(), recorder.status, r.RemoteAddr, cost)
    }
}

type bodyRecorder struct {
    http.ResponseWriter
    status int
}

func (b *bodyRecorder) WriteHeader(code int) {
    b.status = code
    b.ResponseWriter.WriteHeader(code)
}

3. 限流中间件

go-zero 内置了基于 Redis 的分布式限流 rest.WithMaxBytes 等,但更通用的限流可以用 tokenlimit

go
// internal/middleware/ratelimitmiddleware.go
package middleware

import (
    "net/http"

    "github.com/zeromicro/go-zero/core/limit"
    "github.com/zeromicro/go-zero/core/stores/redis"
)

type RateLimitMiddleware struct {
    limiter *limit.PeriodLimit
}

func NewRateLimitMiddleware(rds *redis.Redis, rate int, period int) *RateLimitMiddleware {
    return &RateLimitMiddleware{
        limiter: limit.NewPeriodLimit(period, rate, rds, "rate_limit"),
    }
}

func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 以客户端 IP 作为限流 key
        key := r.RemoteAddr
        code, _ := m.limiter.Take(key)
        if code == limit.OverQuota {
            http.Error(w, "rate limited", http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}

go-zero 还提供 tokenlimit(令牌桶):

go
import "github.com/zeromicro/go-zero/core/limit"

// 每秒生成 100 个令牌,桶容量 200
limiter := limit.NewTokenLimit(100, 200, redisClient)
if limiter.Allow() {
    // 放行
} else {
    // 拒绝
}

4. CORS 中间件

go
// internal/middleware/corsmiddleware.go
package middleware

import (
    "net/http"
)

type CorsMiddleware struct{}

func NewCorsMiddleware() *CorsMiddleware { return &CorsMiddleware{} }

func (m *CorsMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        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",
            "Origin, Content-Type, Accept, Authorization, X-Token")
        w.Header().Set("Access-Control-Max-Age", "86400")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next(w, r)
    }
}

注册为全局中间件:

go
server.Use(middleware.NewCorsMiddleware().Handle)

也可以用 go-zero 内置的 CORS:

go
import "github.com/zeromicro/go-zero/rest/handler"

server := rest.MustNewServer(c.RestConf,
    rest.WithCustomMiddlewares(handler.CorsHandler),
)

三、RPC 拦截器

go-zero 的 RPC 服务基于 gRPC,gRPC 原生支持拦截器(Interceptor)。拦截器分为服务端和客户端两种,go-zero 在此基础上做了链式封装。

1. 服务端拦截器

服务端拦截器签名(一元调用):

go
type UnaryServerInterceptor func(ctx context.Context, req any, info *UnaryServerInfo, handler UnaryHandler) (resp any, err error)

即:在 gRPC 方法调用前后插入逻辑。

go-zero 内置了几个常用拦截器:

  • serverinterceptors.UnaryStatInterceptor:统计指标
  • serverinterceptors.UnaryCrashInterceptor:panic 恢复
  • serverinterceptors.UnaryTraceInterceptor:链路追踪
  • serverinterceptors.UnaryMetricInterceptor:Prometheus 指标
  • serverinterceptors.UnaryRecoverInterceptor:异常恢复

这些拦截器默认开启,开发者一般不需要关心。

自定义服务端拦截器

go
// internal/interceptor/serverinterceptor.go
package interceptor

import (
    "context"

    "github.com/zeromicro/go-zero/core/logx"
)

func LogInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    logx.WithContext(ctx).Infof("[RPC-Server] call %s, req=%+v", info.FullMethod, req)
    resp, err := handler(ctx, req)
    if err != nil {
        logx.WithContext(ctx).Errorf("[RPC-Server] %s failed: %v", info.FullMethod, err)
    }
    return resp, err
}

// 认证拦截器
func AuthInterceptor(secret string) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        // 从 metadata 取 token
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            return nil, status.Error(codes.Unauthenticated, "missing metadata")
        }
        tokens := md.Get("x-token")
        if len(tokens) == 0 || tokens[0] != secret {
            return nil, status.Error(codes.Unauthenticated, "invalid token")
        }
        return handler(ctx, req)
    }
}

注册拦截器:

go
// user.go (main)
func main() {
    // ...
    s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
        pb.RegisterUserServer(grpcServer, srv)
    })

    // 添加服务端拦截器
    s.AddUnaryInterceptors(
        interceptor.LogInterceptor,
        interceptor.AuthInterceptor(c.AuthSecret),
    )

    s.Start()
}

2. 客户端拦截器

客户端拦截器签名:

go
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker UnaryInvoker, opts ...grpc.CallOption) error

自定义客户端拦截器

go
// internal/interceptor/clientinterceptor.go
package interceptor

import (
    "context"
    "time"

    "github.com/zeromicro/go-zero/core/logx"
    "google.golang.org/grpc"
)

func LogClientInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    start := time.Now()
    err := invoker(ctx, method, req, reply, cc, opts...)
    cost := time.Since(start)
    if err != nil {
        logx.WithContext(ctx).Errorf("[RPC-Client] %s failed cost=%v err=%v", method, cost, err)
    } else {
        logx.WithContext(ctx).Infof("[RPC-Client] %s ok cost=%v", method, cost)
    }
    return err
}

// 注入 token 的客户端拦截器
func TokenClientInterceptor(token string) grpc.UnaryClientInterceptor {
    return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        ctx = metadata.AppendToOutgoingContext(ctx, "x-token", token)
        return invoker(ctx, method, req, reply, cc, opts...)
    }
}

注册客户端拦截器:

go
client, err := zrpc.NewClient(c.UserRpc,
    zrpc.WithUnaryClientInterceptor(interceptor.LogClientInterceptor),
    zrpc.WithUnaryClientInterceptor(interceptor.TokenClientInterceptor("my-secret")),
)

3. 链式拦截器

多个拦截器按注册顺序执行,go-zero 内部会用 chain 工具把多个拦截器串起来。执行顺序:

客户端拦截器1 → 客户端拦截器2 → [网络] → 服务端拦截器1 → 服务端拦截器2 → handler

客户端拦截器1 ← 客户端拦截器2 ← [网络] ← 服务端拦截器1 ← 服务端拦截器2 ← handler

4. 内置拦截器一览

go-zero 默认注册了以下拦截器,开发者无需手动添加:

服务端

拦截器作用
UnaryCrashInterceptor捕获 panic,避免进程崩溃
UnaryStatInterceptor统计调用耗时
UnaryTraceInterceptor注入/传播 TraceID
UnaryMetricInterceptor上报 Prometheus 指标
UnaryBreakerInterceptor服务端熔断(基于 googlebreaker)

客户端

拦截器作用
UnaryTraceInterceptor传播 TraceID
UnaryMetricInterceptor上报 Prometheus 指标
UnaryBreakerInterceptor客户端熔断
SheddingInterceptor过载保护

四、中间件与拦截器的区别

维度API 中间件RPC 拦截器
作用域HTTP 服务(rest)gRPC 服务(zrpc)
签名func(http.HandlerFunc) http.HandlerFuncgrpc.UnaryServerInterceptor / UnaryClientInterceptor
注册方式server.Use@server middleware:s.AddUnaryInterceptors / zrpc.WithUnaryClientInterceptor
数据载体http.Request / http.ResponseWritercontext.Context + metadata.MD
典型场景JWT、CORS、日志、限流认证、日志、监控、熔断

设计原则:

  • API 中间件关注 HTTP 层面的横切逻辑(鉴权、CORS、限流、请求日志)
  • RPC 拦截器关注服务间调用的横切逻辑(链路追踪、熔断、调用日志、认证)
  • 链路追踪、监控等通用能力 go-zero 已内置,不要重复造轮子

五、完整示例:带认证和限流的 API + RPC

下面演示一个完整的链路:客户端 → API(JWT + 限流)→ RPC(token 鉴权)。

1. 项目结构

demo/
├── user-rpc/
│   ├── etc/user.yaml
│   ├── user.go
│   └── internal/
│       ├── interceptor/
│       │   └── serverinterceptor.go
│       ├── logic/
│       ├── server/
│       └── svc/
└── user-api/
    ├── etc/user-api.yaml
    ├── user.api
    ├── user.go
    └── internal/
        ├── middleware/
        │   ├── corsmiddleware.go
        │   ├── logmiddleware.go
        │   └── ratelimitmiddleware.go
        ├── logic/
        ├── svc/
        └── ...

2. API 服务配置

etc/user-api.yaml

yaml
Name: user-api
Host: 0.0.0.0
Port: 8888

Auth:
  AccessSecret: "api-secret-key"
  AccessExpire: 86400

Redis:
  Host: 127.0.0.1:6379
  Type: node

RateLimit:
  Rate: 100      # 每秒 100 次
  Period: 1      # 1 秒

UserRpc:
  Etcd:
    Hosts:
      - 127.0.0.1:2379
    Key: user.rpc
  NonBlock: true
  Timeout: 2000

3. API 中间件实现

go
// internal/middleware/ratelimitmiddleware.go
package middleware

import (
    "fmt"
    "net/http"

    "github.com/zeromicro/go-zero/core/limit"
    "github.com/zeromicro/go-zero/core/stores/redis"
)

type RateLimitMiddleware struct {
    limiter *limit.PeriodLimit
}

func NewRateLimitMiddleware(rds *redis.Redis, rate, period int) *RateLimitMiddleware {
    return &RateLimitMiddleware{
        limiter: limit.NewPeriodLimit(period, rate, rds, "api_rate_limit"),
    }
}

func (m *RateLimitMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        key := fmt.Sprintf("%s:%s", r.RemoteAddr, r.URL.Path)
        code, _ := m.limiter.Take(key)
        if code == limit.OverQuota {
            http.Error(w, `{"code":429,"msg":"too many requests"}`, http.StatusTooManyRequests)
            return
        }
        next(w, r)
    }
}
go
// internal/middleware/logmiddleware.go
package middleware

import (
    "net/http"
    "time"

    "github.com/zeromicro/go-zero/core/logx"
)

type LogMiddleware struct{}

func NewLogMiddleware() *LogMiddleware { return &LogMiddleware{} }

func (m *LogMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
        next(rw, r)
        logx.WithContext(r.Context()).Infof("[API] %s %s %d %dms",
            r.Method, r.URL.RequestURI(), rw.status, time.Since(start).Milliseconds())
    }
}

type statusWriter struct {
    http.ResponseWriter
    status int
}

func (w *statusWriter) WriteHeader(code int) {
    w.status = code
    w.ResponseWriter.WriteHeader(code)
}

4. API ServiceContext

go
// internal/svc/servicecontext.go
package svc

import (
    "github.com/zeromicro/go-zero/core/stores/redis"
    "github.com/zeromicro/go-zero/zrpc"

    "user-api/internal/config"
    "user-api/internal/middleware"
    "user-rpc/userclient"
)

type ServiceContext struct {
    Config              config.Config
    UserRpc             userclient.UserClient
    LogMiddleware       rest.Middleware
    RateLimitMiddleware rest.Middleware
}

func NewServiceContext(c config.Config) *ServiceContext {
    rds := redis.MustNewRedis(c.Redis)
    rpcClient := zrpc.MustNewClient(c.UserRpc)
    return &ServiceContext{
        Config:              c,
        UserRpc:             userclient.NewUserClient(rpcClient),
        LogMiddleware:       middleware.NewLogMiddleware().Handle,
        RateLimitMiddleware: middleware.NewRateLimitMiddleware(rds, c.RateLimit.Rate, c.RateLimit.Period).Handle,
    }
}

配置结构扩展:

go
// internal/config/config.go
package config

import (
    "github.com/zeromicro/go-zero/core/stores/redis"
    "github.com/zeromicro/go-zero/rest"
    "github.com/zeromicro/go-zero/zrpc"
)

type Config struct {
    rest.RestConf
    Auth struct {
        AccessSecret string
        AccessExpire int64
    }
    Redis     redis.RedisConf
    RateLimit struct {
        Rate   int
        Period int
    }
    UserRpc zrpc.RpcClientConf
}

5. API .api 文件

api
syntax = "v1"

type (
    LoginRequest {
        Username string `json:"username"`
        Password string `json:"password"`
    }
    LoginResponse {
        Token    string `json:"token"`
        ExpireAt int64  `json:"expireAt"`
    }

    GetUserRequest {
        Id string `path:"id"`
    }
    GetUserResponse {
        Id    int64  `json:"id"`
        Name  string `json:"name"`
        Email string `json:"email"`
    }
)

@server (
    group: auth
    prefix: /api/v1/auth
)
service user-api {
    @handler LoginHandler
    post /login (LoginRequest) returns (LoginResponse)
}

@server (
    group: user
    prefix: /api/v1
    jwt: Auth
    middleware: LogMiddleware,RateLimitMiddleware
)
service user-api {
    @handler GetUserHandler
    get /user/:id (GetUserRequest) returns (GetUserResponse)
}

6. RPC 服务端拦截器

go
// user-rpc/internal/interceptor/serverinterceptor.go
package interceptor

import (
    "context"

    "github.com/zeromicro/go-zero/core/logx"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
)

// 认证拦截器:校验 x-rpc-token
func AuthInterceptor(secret string) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
        md, ok := metadata.FromIncomingContext(ctx)
        if !ok {
            return nil, status.Error(codes.Unauthenticated, "missing metadata")
        }
        tokens := md.Get("x-rpc-token")
        if len(tokens) == 0 || tokens[0] != secret {
            return nil, status.Error(codes.Unauthenticated, "invalid rpc token")
        }
        return handler(ctx, req)
    }
}

// 日志拦截器
func LogInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
    logx.WithContext(ctx).Infof("[RPC] call %s", info.FullMethod)
    resp, err := handler(ctx, req)
    if err != nil {
        logx.WithContext(ctx).Errorf("[RPC] %s failed: %v", info.FullMethod, err)
    }
    return resp, err
}

7. RPC main 注册拦截器

go
// user-rpc/user.go
package main

import (
    "flag"
    "fmt"

    "user-rpc/internal/config"
    "user-rpc/internal/interceptor"
    "user-rpc/internal/server"
    "user-rpc/internal/svc"
    "user-rpc/pb"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/zrpc"
    "google.golang.org/grpc"
)

var configFile = flag.String("f", "etc/user.yaml", "the config file")

func main() {
    flag.Parse()
    var c config.Config
    conf.MustLoad(*configFile, &c)

    ctx := svc.NewServiceContext(c)
    srv := server.NewUserServer(ctx)

    s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
        pb.RegisterUserServer(grpcServer, srv)
    })

    // 注册服务端拦截器
    s.AddUnaryInterceptors(
        interceptor.LogInterceptor,
        interceptor.AuthInterceptor(c.RpcToken),
    )
    defer s.Stop()

    fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
    s.Start()
}

RPC 配置扩展:

go
// user-rpc/internal/config/config.go
package config

import "github.com/zeromicro/go-zero/zrpc"

type Config struct {
    zrpc.RpcServerConf
    RpcToken string
}

etc/user.yaml

yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
RpcToken: "rpc-secret-token"
Etcd:
  Hosts:
    - 127.0.0.1:2379
  Key: user.rpc

8. API 客户端注入 RPC token

go
// user-api/internal/svc/servicecontext.go(更新)
func NewServiceContext(c config.Config) *ServiceContext {
    rds := redis.MustNewRedis(c.Redis)
    rpcClient := zrpc.MustNewClient(c.UserRpc,
        zrpc.WithUnaryClientInterceptor(tokenClientInterceptor(c.UserRpcToken)),
    )
    // ...
}

func tokenClientInterceptor(token string) grpc.UnaryClientInterceptor {
    return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        ctx = metadata.AppendToOutgoingContext(ctx, "x-rpc-token", token)
        return invoker(ctx, method, req, reply, cc, opts...)
    }
}

API 配置加 UserRpcToken

yaml
UserRpcToken: "rpc-secret-token"

9. 验证流程

  1. 客户端调用 POST /api/v1/auth/login 获取 JWT token
  2. 客户端带 Authorization: Bearer <token> 调用 GET /api/v1/user/1
  3. API 中间件链:JWT 校验 → 日志 → 限流 → handler
  4. handler 调用 Logic,Logic 调用 RPC(客户端拦截器注入 x-rpc-token)
  5. RPC 服务端拦截器链:日志 → token 鉴权 → logic
  6. 响应逐层返回

测试:

bash
# 登录
TOKEN=$(curl -s -X POST http://localhost:8888/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}' | jq -r '.token')

# 调用需要鉴权的接口
curl http://localhost:8888/api/v1/user/1 \
  -H "Authorization: Bearer $TOKEN"

六、常见中间件最佳实践

1. 中间件应该轻量

中间件在每个请求都会执行,不要在里面做重逻辑(如 DB 查询、远程调用)。如果必须做(如从 Redis 校验 token),务必加本地缓存。

2. 错误处理要规范

中间件里的错误要返回标准 HTTP 状态码,并保持响应格式一致:

go
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if !checkToken(r) {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusUnauthorized)
            w.Write([]byte(`{"code":401,"msg":"unauthorized"}`))
            return
        }
        next(w, r)
    }
}

3. 区分公开与鉴权路由

把无需鉴权的接口(登录、健康检查、Swagger 文档)放到独立的 @server 块,不带 jwt 和鉴权中间件。

4. 限流要分维度

  • 全局限流:保护服务整体
  • 接口限流:保护关键接口
  • 用户限流:防止单用户刷接口

七、小结

本篇讲解了 go-zero 的中间件与拦截器机制。要点回顾:

  • API 中间件分全局(server.Use)和路由组(@server middleware:)两种注册方式
  • 常用中间件:JWT、日志、限流、CORS,可以自定义实现
  • RPC 拦截器分服务端(s.AddUnaryInterceptors)和客户端(zrpc.WithUnaryClientInterceptor
  • go-zero 内置了链路追踪、监控、熔断等拦截器,无需手动添加
  • 中间件关注 HTTP 层,拦截器关注 RPC 层,职责互补
  • 完整示例演示了 API(JWT + 限流)→ RPC(token 鉴权)的链路

下一篇我们将进入数据库与 Redis 集成,讲解 sqlx、GORM、redisz、go-zero 内置缓存的使用。