Appearance
中间件与拦截器
本篇讲解 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,
}
}注意:@server 里 middleware 写的是字段名(去掉包名前缀,且首字母小写转大写后的形式),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 ← handler4. 内置拦截器一览
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.HandlerFunc | grpc.UnaryServerInterceptor / UnaryClientInterceptor |
| 注册方式 | server.Use 或 @server middleware: | s.AddUnaryInterceptors / zrpc.WithUnaryClientInterceptor |
| 数据载体 | http.Request / http.ResponseWriter | context.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: 20003. 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.rpc8. 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. 验证流程
- 客户端调用
POST /api/v1/auth/login获取 JWT token - 客户端带
Authorization: Bearer <token>调用GET /api/v1/user/1 - API 中间件链:JWT 校验 → 日志 → 限流 → handler
- handler 调用 Logic,Logic 调用 RPC(客户端拦截器注入 x-rpc-token)
- RPC 服务端拦截器链:日志 → token 鉴权 → logic
- 响应逐层返回
测试:
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 内置缓存的使用。