证书认证
1. 为什么需要证书认证?
普通的 gRPC 连接(如 grpc.WithInsecure()
)是明文传输的,数据可能被第三方监听或篡改。证书认证通过 TLS 加密 确保通信安全,类似 HTTPS 对 HTTP 的增强。
2. 核心概念:证书与密钥
- 私钥(
.key
):保密文件,用于签名和加密。 - 证书(
.crt
):公开文件,包含公钥和身份信息(如域名)。 - 根证书(CA):权威机构颁发的证书,用于验证其他证书的合法性。
3. 单向认证(服务器认证)
步骤 1:生成服务器证书
# 生成服务器私钥
openssl genrsa -out server.key 2048
# 生成自签名证书(CN=服务器域名)
openssl req -new -x509 -days 3650 \
-subj "/C=GB/L=China/O=grpc-server/CN=server.grpc.io" \
-key server.key -out server.crt
步骤 2:服务器配置证书
// 服务器代码
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
server := grpc.NewServer(grpc.Creds(creds))
步骤 3:客户端验证服务器证书
// 客户端代码
creds, err := credentials.NewClientTLSFromFile("server.crt", "server.grpc.io")
conn, err := grpc.Dial("localhost:5000", grpc.WithTransportCredentials(creds))
关键点:
- 客户端需要预先知道服务器证书(
server.crt
)。 - 服务器证书中的 CN(Common Name) 必须与客户端连接的域名一致(如
server.grpc.io
)。
4. 双向认证(服务器 + 客户端互认)
单向认证只能确保客户端访问的是“真正的服务器”,但服务器无法确认客户端身份。双向认证通过 根证书(CA) 解决这个问题。
步骤 1:生成根证书(CA)
openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 \
-subj "/C=GB/L=China/O=gobook/CN=github.com" \
-key ca.key -out ca.crt
步骤 2:用 CA 签名服务器和客户端证书
# 服务器证书签名
openssl req -new -subj "/C=GB/L=China/O=server/CN=server.io" -key server.key -out server.csr
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -in server.csr -out server.crt
# 客户端证书签名
openssl req -new -subj "/C=GB/L=China/O=client/CN=client.io" -key client.key -out client.csr
openssl x509 -req -sha256 -CA ca.crt -CAkey ca.key -CAcreateserial -days 3650 -in client.csr -out client.crt
步骤 3:服务器配置(验证客户端)
certificate, err := tls.LoadX509KeyPair("server.crt", "server.key")
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("ca.crt")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ClientAuth: tls.RequireAndVerifyClientCert, // 启用客户端验证
ClientCAs: certPool,
})
server := grpc.NewServer(grpc.Creds(creds))
步骤 4:客户端配置(验证服务器)
certificate, err := tls.LoadX509KeyPair("client.crt", "client.key")
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("ca.crt")
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{certificate},
ServerName: "server.io", // 必须与服务器证书的 CN 一致
RootCAs: certPool,
})
conn, err := grpc.Dial("localhost:5000", grpc.WithTransportCredentials(creds))
关键点:
- 根证书(
ca.crt
)是信任的基础,客户端和服务器都需要它。 - 服务器通过
ClientAuth: tls.RequireAndVerifyClientCert
强制验证客户端证书。 - 客户端通过
RootCAs
验证服务器证书是否由可信 CA 签发。
5. 为什么需要 CA 证书?
如果直接使用自签名证书(如第一部分),客户端必须预先知道服务器证书。但在复杂环境中,证书传输可能被篡改。通过 CA 证书:
- CA 证书是权威的(如 Let’s Encrypt),或自己创建的根证书。
- 服务器和客户端证书都由 CA 签名。
- 客户端/服务器只需验证对方证书是否由可信 CA 签发,无需预先交换具体证书。
总结
- 单向认证:服务器有证书,客户端验证服务器身份。
- 双向认证:服务器和客户端都有证书,双方互相验证身份,安全性更高。
- CA 证书:解决证书传输过程中的篡改问题,是企业级安全的标准做法。
gRPC 截取器(Interceptor)详解
1. 什么是截取器?为什么需要它?
截取器(Interceptor)是 gRPC 中处理请求的中间层,类似 Web 框架中的「中间件」。它的核心作用是:
- 在请求到达业务逻辑前执行预处理(如认证、日志、参数验证)
- 在业务逻辑处理后执行后处理(如响应修改、错误封装)
- 实现非侵入式的横切功能(不修改业务代码即可增强服务能力)
2. 普通方法截取器(UnaryInterceptor)的核心原理
基本结构
// 拦截器函数签名
func interceptor(
ctx context.Context, // 请求上下文
req interface{}, // 请求参数
info *grpc.UnaryServerInfo, // 方法元信息(如方法名)
handler grpc.UnaryHandler, // 原始方法处理器
) (resp interface{}, err error) {
// 前置处理(请求到达业务逻辑前)
log.Println("请求开始:", info.FullMethod)
// 调用原始业务方法
resp, err = handler(ctx, req)
// 后置处理(业务逻辑执行后)
if err != nil {
log.Println("请求失败:", err)
} else {
log.Println("请求成功")
}
return resp, err
}
关键参数解析
ctx context.Context
:包含请求的超时、取消信号等上下文信息req interface{}
:gRPC 方法的请求参数(如*HelloRequest
)info *UnaryServerInfo
:包含当前调用的方法名(如/package.Service/Method
)handler grpc.UnaryHandler
:指向原始业务方法的函数指针,调用它才会执行实际逻辑
3. 截取器的典型应用场景
(1)Token 认证(替代手动验证)
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从上下文提取 Token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "缺少认证信息")
}
// 验证 Token(如 JWT)
token := md.Get("authorization")[0]
if err := verifyToken(token); err != nil {
return nil, status.Errorf(codes.Unauthenticated, "Token 无效: %v", err)
}
// 认证通过,继续处理请求
return handler(ctx, req)
}
(2)异常捕获(防止服务崩溃)
func recoveryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转换为 gRPC 错误
return status.Errorf(codes.Internal, "服务内部错误: %v", r)
}
}()
return handler(ctx, req)
}
(3)调用链跟踪(分布式追踪)
func tracingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 从请求头提取 Trace ID
traceID := extractTraceID(ctx)
// 记录调用开始
log.Println("开始调用:", info.FullMethod, "TraceID:", traceID)
// 执行方法
resp, err := handler(ctx, req)
// 记录调用结束
if err != nil {
log.Println("调用失败:", info.FullMethod, "TraceID:", traceID, "Error:", err)
} else {
log.Println("调用成功:", info.FullMethod, "TraceID:", traceID)
}
return resp, err
}
4. 链式截取器:多个功能的组合使用
gRPC 原生只支持单个截取器,而 go-grpc-middleware 包提供了链式调用能力,允许同时使用多个截取器:
安装依赖
go get github.com/grpc-ecosystem/go-grpc-middleware
链式配置示例
import (
"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap"
"github.com/grpc-ecosystem/go-grpc-middleware/recovery"
)
func main() {
// 创建多个拦截器
logInterceptor := grpc_zap.UnaryServerInterceptor(zapLogger)
recoveryInterceptor := grpc_recovery.UnaryServerInterceptor()
authInterceptor := AuthInterceptor() // 自定义认证拦截器
// 链式组合拦截器
server := grpc.NewServer(
// 一元 RPC 拦截器链
grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
logInterceptor, // 日志拦截器(先执行)
authInterceptor, // 认证拦截器
recoveryInterceptor, // 异常捕获拦截器(后执行)
)),
// 流 RPC 拦截器链(类似)
grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
// 流相关拦截器...
)),
)
// 注册服务...
server.Serve(lis)
}
链式执行顺序
拦截器按传入顺序执行:
- 前置处理按
filter1 → filter2 → ... → 业务方法
顺序执行 - 后置处理按
... → filter2 → filter1
逆序执行filter1前置 → filter2前置 → 业务方法 → filter2后置 → filter1后置
5. 流方法截取器(StreamInterceptor)简介
流方法(如客户端流、服务器流、双向流)的拦截器与普通方法类似,但接口不同:
func streamInterceptor(
srv interface{},
stream grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
log.Println("流方法调用开始:", info.FullMethod)
// 包装原始 StreamServer,实现自定义逻辑
wrappedStream := &wrappedServerStream{
ServerStream: stream,
}
err := handler(srv, wrappedStream)
log.Println("流方法调用结束:", info.FullMethod, "Error:", err)
return err
}
// 自定义 StreamServer,可拦截流操作
type wrappedServerStream struct {
grpc.ServerStream
}
// 示例:重写 SendMsg 方法以记录响应
func (w *wrappedServerStream) SendMsg(m interface{}) error {
log.Println("发送流响应:", m)
return w.ServerStream.SendMsg(m)
}
6. 截取器的核心价值
- 关注点分离:将认证、日志等非业务逻辑与核心功能解耦
- 代码复用:一套拦截器可应用于所有服务方法
- 可扩展性:新增功能(如限流)时无需修改业务代码
- 标准化:统一处理流程,避免各方法重复实现相同逻辑
总结
截取器是 gRPC 中最强大的扩展机制之一,通过它可以在不侵入业务逻辑的前提下,为服务添加认证、监控、追踪等关键功能。在实际项目中,结合 go-grpc-middleware
实现链式拦截器,是构建健壮、可维护的微服务的最佳实践。