2. Protobuf 基础与服务定义

本章是构建 gRPC 权限验证系统的契约层。你将回顾 Protobuf 的核心语法,掌握如何将认证上下文(如 Token、用户角色)显式定义在 RPC 服务签名中,并最终生成 Go 语言代码。一个清晰、强类型的 Protobuf 文件是后续所有权限拦截与校验的基石。

2.1 Protobuf 语法快速回顾

Protocol Buffers(Protobuf)是 gRPC 的接口定义语言(IDL)。以下是你必须掌握的核心语法要素:

语法要素 关键字/示例 说明
语法版本 syntax = "proto3"; 必须位于文件第一行,声明使用 proto3 语法。
包声明 package auth.v1; 防止命名冲突,生成 Go 代码时映射为 package 名。
导入 import "google/protobuf/timestamp.proto"; 复用已有类型,如 Timestamp、Empty。
消息类型 message User { string id = 1; } 定义结构化数据,字段有唯一编号。
标量类型 string, int32, bool, bytes 基础数据类型,映射到 Go 原生类型。
枚举 enum Role { ADMIN = 0; USER = 1; } 定义有限取值集合,第一个枚举值必须为 0。
字段编号 int32 age = 2; 1~15 用 1 字节编码,16~2047 用 2 字节,高频字段优先使用小编号。
repeated repeated string tags = 3; 表示字段可重复(数组/切片)。
map map<string, string> metadata = 4; 定义键值对映射。
oneof oneof payload { string text = 5; bytes data = 6; } 一组字段中同时只能设置一个。
最佳实践:字段编号一旦被使用,不应更改或复用。删除字段时,应使用 reserved 关键字标记已删除的编号,防止未来误用导致数据损坏。

2.2 定义带认证上下文的 RPC 服务

在权限验证场景中,每个 RPC 请求都需要携带调用者的身份与凭证。我们有两种主流设计模式:

  • 模式 A:显式认证消息 —— 将认证信息作为每个 RPC 请求消息的第一个字段,或封装在统一的 Request 包裹中。这种方式契约清晰,但会增加消息嵌套。
  • 模式 B:gRPC 拦截器 + Metadata —— 认证信息通过 gRPC 的 Metadata(类似 HTTP Header)传递,服务定义中不体现。这种方式服务定义更干净,但认证逻辑完全隐式,依赖拦截器实现。

本课程采用 模式 A + 模式 B 混合:在 Protobuf 中显式定义 AuthContext 消息,并将其嵌入到需要权限校验的 RPC 请求中,同时利用拦截器从 Metadata 中提取 Token 并填充该上下文。这样既保持了契约的可见性,又实现了认证逻辑的复用。

以下是一个完整的 Protobuf 文件示例,定义了一个带认证上下文的权限验证服务:

syntax = "proto3";

package auth.v1;

option go_package = "github.com/yourorg/auth-proto/gen/go/auth/v1;authv1";

// 认证上下文:每个需要权限的请求都应携带此信息
message AuthContext {
  string user_id = 1;          // 用户唯一标识
  string token = 2;            // 原始 JWT 或 Session Token
  repeated string roles = 3;   // 用户角色列表,如 ["admin", "editor"]
  map<string, string> claims = 4; // 额外的声明字段
}

// 资源操作请求
message ResourceRequest {
  AuthContext auth = 1;        // 认证上下文,字段编号 1
  string resource_id = 2;      // 目标资源 ID
  string action = 3;           // 操作类型:read / write / delete
}

// 资源操作响应
message ResourceResponse {
  bool allowed = 1;            // 是否允许操作
  string message = 2;          // 提示信息
}

// 权限验证服务定义
service PermissionService {
  // CheckPermission 检查用户是否有权对指定资源执行操作
  rpc CheckPermission(ResourceRequest) returns (ResourceResponse);
}
设计要点:
  • AuthContext 作为独立消息,可在多个 RPC 中复用。
  • 字段编号 1 留给 AuthContext,保持一致性。
  • repeated string roles 支持多角色判断。
  • map<string, string> claims 提供扩展性,无需修改消息结构即可传递自定义声明。

2.3 认证上下文传递流程

下图展示了客户端携带 Token 发起请求,到服务端拦截器解析并填充 AuthContext 的完整流程:

认证上下文传递流程 客户端 携带 Token 发起 RPC Metadata: token gRPC 拦截器 解析 Token → 填充 AuthContext AuthContext 已填充 业务服务 直接使用 auth 字段 关键说明 1. 客户端将 Token 放入 gRPC Metadata(key: "authorization")。 2. 服务端拦截器从 Metadata 提取 Token,验证后填充 ResourceRequest.auth 字段。 3. 业务服务无需关心 Token 解析,直接使用 AuthContext 做权限判断。 Protobuf 契约:ResourceRequest.auth 字段 类型: AuthContext | 字段编号: 1 | 必填 契约约束

2.4 生成 Go 代码

定义好 Protobuf 文件后,使用 protoc 编译器生成 Go 代码。以下是标准步骤:

  1. 安装工具链:
    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
  2. 编写编译脚本(Makefile 或 shell):
    protoc --proto_path=proto \
           --go_out=gen/go \
           --go_opt=paths=source_relative \
           --go-grpc_out=gen/go \
           --go-grpc_opt=paths=source_relative \
           proto/auth/v1/permission.proto
  3. 生成的 Go 文件结构:
    gen/go/auth/v1/
    ├── permission.pb.go          # 消息类型:AuthContext, ResourceRequest, ResourceResponse
    └── permission_grpc.pb.go     # 服务接口:PermissionServiceServer, PermissionServiceClient
常见错误:
  • protoc-gen-go 版本与 google.golang.org/protobuf 不匹配,导致生成代码编译失败。请始终使用 go install 安装最新稳定版。
  • go_package 选项未正确设置,导致生成的包路径与项目结构不一致。建议使用 paths=source_relative 选项。
  • 忘记导入 google/protobuf/empty.proto 却使用了 Empty 类型。

2.5 生成的 Go 代码核心结构

以下是你将直接使用的 Go 接口与消息类型:

生成产物 Go 类型/接口 用途
消息 AuthContext 包含 UserId、Token、Roles、Claims 字段
消息 ResourceRequest 包含 AuthContext 和业务参数
消息 ResourceResponse 包含 Allowed 和 Message 字段
服务端接口 PermissionServiceServer 必须实现 CheckPermission 方法
客户端接口 PermissionServiceClient 提供 CheckPermission 调用方法
注册函数 RegisterPermissionServiceServer 将服务实现注册到 gRPC Server

服务端接口定义如下(由 protoc-gen-go-grpc 生成):

// PermissionServiceServer 是服务端需要实现的接口
type PermissionServiceServer interface {
  CheckPermission(context.Context, *ResourceRequest) (*ResourceResponse, error)
  mustEmbedUnimplementedPermissionServiceServer()
}

客户端调用示例:

// 创建客户端连接
conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()

client := authv1.NewPermissionServiceClient(conn)

// 构造带认证上下文的请求
req := &authv1.ResourceRequest{
  Auth: &authv1.AuthContext{
    UserId: "user-123",
    Token:  "eyJhbGciOiJIUzI1NiIs...",
    Roles:  []string{"admin"},
  },
  ResourceId: "resource-456",
  Action:     "delete",
}

// 发起 RPC 调用
resp, err := client.CheckPermission(ctx, req)
提示:在实际生产环境中,客户端不应手动构造 AuthContext。认证上下文的填充应由 gRPC 客户端拦截器自动完成,从本地存储的 Token 中提取并注入。我们将在第 5 章详细实现客户端拦截器。

2.6 本章小结

通过本章的学习,你已经掌握了:

  • Protobuf 核心语法,特别是消息定义、字段编号规则和 repeated/map 的使用。
  • 如何将认证上下文显式定义在 RPC 服务契约中,实现权限验证的契约化。
  • 认证上下文从客户端到服务端的完整传递流程。
  • 使用 protoc 生成 Go 代码,并理解生成的接口与消息类型。