GRPC支持protocol结构化数据校验
使用protoc-gen-validate
PGV是一个protoc插件,用于生成多语言消息验证器。虽然
proto buffer
有效地保证了结构化数据的类型,但它们不能对值执行语义规则。这个插件为protoc生成的代码添加了支持,以验证这种约束。他会在 protocol 生成我们的grpc的代码的时候,会读取一些protoc-gen-validate插件支持的数据结构规则,额外去生成校验的代码!来完成校验的功能!
导入PGV扩展,用约束规则注释他们proto文件中的消息和字段:
安装去 github
clone 一下 然后进去make build
~/xx cd protoc-gen-validate
~/env/protoc-gen-validate main ± make build
~/env/protoc-gen-validate main ± ...... # 安装输出
~/env/protoc-gen-validate main ± cd ~/go/bin
~/go/bin ls
air go-outliner gopls protoc-gen-go-grpc
dlv gomodifytags gotests protoc-gen-validate
dlv-dap gopkgs impl staticcheck
go-outline goplay protoc-gen-go
还要去copy一下 validate.proto 文件到自己的项目本地,因为校验的一些 protocol 结构在这个文件中! 需要在自己的proto文件引用!
syntax = "proto3"; // 版本
package hello; //包声明
import "_validator/validate.proto"; // 引用 validate.proto
option go_package = "./user";
message AimUser {
string name = 1 [(validate.rules).string.min_len = 6]; // 这里指定了 name 的最小长度是6
}
service User {
rpc SayHi(AimUser) returns(AimUser); //定义服务的某一个方法,接受的参数,和返回值
}
这样就可以来生成代码了!
protoc -I=. --go_out=./ --go-grpc_out=./ --validate_out="lang=go:." *.proto
protoc
: Protocol Buffers 编译器的可执行文件名。-I=
: 指定包含.proto文件的目录。这里的.
表示当前目录。--go_out=./
: 生成 Go 代码文件的输出目录。这里的./
表示当前目录。--go-grpc_out=./
: 生成 gRPC 相关的 Go 代码文件的输出目录。这里的./
表示当前目录。--validate_out="lang=go:."
: 生成用于验证消息的 Go 代码文件的输出目录。这里的lang=go:.
指定生成 Go 代码,并将其输出到当前目录。*.proto
: 要编译的.proto文件的通配符。这里使用*.proto
来匹配当前目录下的所有.proto文件。
➜ microway git:(protoc-gen-validate) ✗ tree
├── _validator
│ └── validate.proto # github 的 validate文件!
├── client
│ └── client.go
├── go.mod
├── go.sum
├── grpc_User.proto
├── server
│ └── server.go
└── user
├── grpc_User.pb.go # 消息体生成的结构体代码
├── grpc_User.pb.validate.go # protocol 生成的 grpc服务客户端服务的逻辑代码
└── grpc_User_grpc.pb.go # 一些额外的验证代码,针对我们加入规则验证的结构体,添加了一些验证方法
当对参数进行校验的时候提供了两个validator
方法!
ValidateAll()
一次性校验所有的参数,全部返回Validate()
逐条校验返回!
func (m *AimUser) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
if utf8.RuneCountInString(m.GetName()) < 6 {
err := AimUserValidationError{
field: "Name",
reason: "value length must be at least 6 runes",
}
// 如果不是全部 就单次返回这一个error 否则就是 errors 切片!
if !all {
return err
}
errors = append(errors, err)
}
if len(errors) > 0 {
return AimUserMultiError(errors)
}
return nil
}
测试校验不通过!
aimUser := &user.AimUser{Name: "村望老弟!"}
err = aimUser.ValidateAll()
if err != nil {
log.Fatalf(err.Error())
}
测试校验通过
服务端断言校验GRPC的请求数据!
上面我们通过调用客户端的
validate
方法去进行protocol数据的校验!
如果有的客户端调用之前没有执行呢!(家人们谁懂啊!)
那么我们希望我们的服务端也可以对请求的数据进行一次校验!
思路很简单:我们根据鸭子类型的特点!自定义实现了 ValidateAll
和 Validate
的方法!然后断言看看能不能断言为这个类型!
如果成功说明是具有这 ValidateAll 和 Validate 方法的请求参数!那么我们就去执行这些方法来校验参数!
type Validator interface {
Validate() error
ValidateAll() error
}
// UnaryServerInterceptor 实现拦截器的 UnaryServerInterceptor 方法里面就是具体的拦截器内部要做的拦截逻辑!
func (i *CustomInterceptor) UnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// 在处理请求之前执行的逻辑
log.Println("CustomInterceptor: Before handling the request")
if outgoingContext, b := metadata.FromIncomingContext(ctx); b {
tokenSlice := outgoingContext.Get("token")
if len(tokenSlice) < 1 || tokenSlice[0] != "xxx" {
return nil, status.Error(codes.Unauthenticated, "无认证信息")
}
} else {
return nil, status.Error(codes.Unauthenticated, "无认证信息")
}
//🙆 断言为MyValidator然后执行校验!
if r, ok := req.(MyValidator); ok {
err := r.ValidateAll()
if err != nil {
return nil, status.Error(codes.InvalidArgument, "参数错误"+err.Error())
}
}
// 调用下一个拦截器或服务处理程序
resp, err := handler(ctx, req)
// 在处理请求后执行的逻辑
log.Println("CustomInterceptor: After handling the request")
return resp, err
}
通过的测试
评论区