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 的完整流程:
2.4 生成 Go 代码
定义好 Protobuf 文件后,使用 protoc 编译器生成 Go 代码。以下是标准步骤:
- 安装工具链:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - 编写编译脚本(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 - 生成的 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 代码,并理解生成的接口与消息类型。