跳到主要内容

Go 中实现 gRPC 服务端拦截器

使用场景

访问日志

错误日志

什么是服务端拦截器?

服务端拦截器在服务端处理 RPC 请求的流程中作为中间层加入。我在 gRPC 中可以通过拦截器在请求进入服务端方法前后执行特定逻辑。拦截器可用于一元调用和流式调用。我这里集中说明一元调用的拦截器。服务端拦截器能够实现认证与授权、日志记录与监控、请求响应数据转换以及统一错误处理等功能。

以认证为例,我可以在拦截器中解析请求的元数据,从中提取 JWT 令牌或 API 密钥,然后检查其有效性和权限。当令牌验证通过后才让请求继续执行服务端的业务方法。如果验证失败,拦截器中可以直接返回 gRPC 标准错误并终止后续处理。

对于日志与监控,我可以在拦截器中记录当前请求的方法名、请求时间以及响应时间,将这些数据发送至日志系统或度量系统中,方便后续分析和故障诊断。

对于请求响应数据转换,我可以在拦截器中对请求参数进行预处理,比如统一补全某些缺省字段,或在响应结果返回前进行格式化处理。

对于错误处理,我可以在拦截器中拦截服务方法返回的错误,将其统一转换为标准的 gRPC 错误码和错误消息,以便客户端统一处理。

在 Go 中实现一元服务端拦截器的步骤

定义拦截器函数

拦截器函数需符合 grpc.UnaryServerInterceptor 的签名

func (context context.Context, requestData interface{}, info *grpc.UnaryServerInfo, nextHandler grpc.UnaryHandler) (interface{}, error)

我可以在这个函数中实现逻辑。例如实现验证令牌的拦截器函数时,我会从 context 的元数据中解析令牌,根据业务逻辑检查其有效性,通过后再调用 nextHandler(context, requestData) 将请求交给后续处理。如果不通过则直接返回错误。

如下为基于令牌验证的拦截器示例

import (
"context"
"errors"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
)

func checkToken(context context.Context, requestData interface{}, info *grpc.UnaryServerInfo, nextHandler grpc.UnaryHandler) (interface{}, error) {
meta, ok := metadata.FromIncomingContext(context)
if !ok {
return nil, status.Error(codes.Unauthenticated, "无法获取元数据")
}

tokens := meta.Get("authorization")
if len(tokens) == 0 || !validateToken(tokens[0]) {
return nil, status.Error(codes.Unauthenticated, "无效的令牌")
}

return nextHandler(context, requestData)
}

func validateToken(token string) bool {
// 这里可对 token 进行校验,比如检查签名、过期时间、用户权限等
// 实战中可结合 JWT 库来解析并校验令牌有效性
return token == "valid-token"
}

日志拦截器可在请求处理前后记录请求方法名和处理耗时

import (
"log"
"time"
"google.golang.org/grpc"
)

func logRequest(context context.Context, requestData interface{}, info *grpc.UnaryServerInfo, nextHandler grpc.UnaryHandler) (interface{}, error) {
startTime := time.Now()
response, err := nextHandler(context, requestData)
duration := time.Since(startTime)
log.Printf("方法: %s 耗时: %v 错误: %v", info.FullMethod, duration, err)
return response, err
}

上例中在调用 nextHandler 前后记录了方法名和执行耗时,同时也将后续处理返回的错误信息一起记录。

链接多个拦截器

当需要串联多个拦截器时,可使用 grpc_middleware.ChainUnaryServer 将多个拦截器组合成一个拦截器链。链中拦截器的执行顺序与传入顺序一致。这样我可以先执行认证拦截器,再执行日志拦截器,最后再交给业务逻辑处理。

import (
"github.com/grpc-ecosystem/go-grpc-middleware"
)

interceptorChain := grpc_middleware.ChainUnaryServer(
checkToken,
logRequest,
)

将拦截器应用到 gRPC 服务器

创建 gRPC 服务端实例时将拦截器链添加到服务器选项中

import "google.golang.org/grpc"

options := []grpc.ServerOption{
grpc.UnaryInterceptor(interceptorChain),
}
gServer := grpc.NewServer(options...)

实际部署中,我可在这里根据需要添加更多拦截器,或对拦截器的执行顺序进行合理安排。

如何确保拦截器的高质量实现

我在编写拦截器时要避免在其中写入阻塞性逻辑或耗时过长的操作,以免影响请求处理效率。日志和认证检查通常是快速执行的操作,可使用缓存或预解析策略提升性能。对于错误处理,建议将服务端错误统一转换为 gRPC 状态码,并在拦截器中实现标准化的错误返回。对于日志拦截器,可结合结构化日志工具记录详细信息,以便后续追踪请求调用链路和分析问题。

在实际生产环境中,如果我需要动态启用或禁用某些拦截逻辑,可在拦截器中添加可配置参数或全局控制变量,从而灵活控制其行为。
同样在验证令牌时可结合外部认证系统,以微服务方式独立实现权限模块,再在拦截器中调用该认证服务,从而实现更高的灵活性和扩展性。