gRPC参数校验之grpc_validator

注:本文前提是已熟练使用gRPC

在http请求中,我们需要为每个请求参数进行数据验证,以防恶意请求和参数错误的情况

例如在Gin框架中,提供了github.com/go-playground/validator/v10该库作为验证器,我们只需要在结构体中进行规则绑定

type UserCreateReq struct {
    Name  string `json:"name" binding:"required" label:"姓名"`
    Intro string `json:"intro" binding:"required,max=60,min=1" label:"介绍"`
}

然后在解析参数时就会自动进行数据校验,而不需要我们在接收到参数后通过if进行判断了

req := &user.UserCreateReq{}
if err := ctx.ShouldBind(req); err != nil {
    response.Fail(ctx, constant.ParameterIllegal, xerror.Trans(err))
    return
}

注:xerror.Trans(err) 是封装的一个对错误内容进行翻译的方法

那么在gRPC中我们如何实现这种自动的参数校验呢?

本文将基于grpc_middleware中的 validator 实现


grpc_validator

在上面链接中 doc.go 文件中提到

// While it is generic, it was intended to be used with https://github.com/mwitkow/go-proto-validators,
// a Go protocol buffers codegen plugin that creates the `Validate` methods (including nested messages)
// based on declarative options in the `.proto` files themselves. For example:

syntax = "proto3";
package validator.examples;
import "github.com/mwitkow/go-proto-validators/validator.proto";

message InnerMessage {
    // some_integer can only be in range (1, 100).
    int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}];
    // some_float can only be in range (0;1).
    double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}];
}

message OuterMessage {
    // important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).
    string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}];
    // proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.
    InnerMessage inner = 2 [(validator.field) = {msg_exists : true}];
}

正如第一句提到的一样,我们需要先引入https://github.com/mwitkow/go-proto-validators

go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators 

# 该命令会在我们的${GOPATH}/bin 下生成一个protoc-gen-govalidators可执行文件,用来自动生成我们的验证规则
go install github.com/mwitkow/go-proto-validators/protoc-gen-govalidators 

注:go install 安装是必须的,后续需要基于govalidators生成pb文件

然后编写我们的proto文件

syntax = "proto3";

option go_package = "../simple_v";
option java_multiple_files = true;
option java_package = "io.grpc.examples.protobuf";
option java_outer_classname = "SimpleVProto";

package protobuf;

import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto";

message InnerMessage {
    // some_integer can only be in range (1, 100).
    int32 some_integer = 1 [(validator.field) = {int_gt: 0, int_lt: 100}];
    // some_float can only be in range (0;1).
    double some_float = 2 [(validator.field) = {float_gte: 0, float_lte: 1}];
}

message OuterMessage {
    // important_string must be a lowercase alpha-numeric of 5 to 30 characters (RE2 syntax).
    string important_string = 1 [(validator.field) = {regex: "^[a-z]{2,5}$"}];
    // proto3 doesn't have `required`, the `msg_exist` enforces presence of InnerMessage.
    InnerMessage inner = 2 [(validator.field) = {msg_exists : true}];
}

service SimpleVServer {
    rpc TestSimpleV (InnerMessage) returns (OuterMessage) {}
}

使用命令生成pb文件

protoc --proto_path=$GOPATH/pkg/mod --proto_path=. --go_out=. --go-grpc_out=. --govalidators_out=.  ./simple.proto 

这一步会在当前目录下生成一个 simple.validator.pb.go 文件,这里面就是我们的验证规则

func (this *InnerMessage) Validate() error {
    if !(this.SomeInteger > 0) {
        return github_com_mwitkow_go_proto_validators.FieldError("SomeInteger", fmt.Errorf(`value '%v' must be greater than '0'`, this.SomeInteger))
    }
    if !(this.SomeInteger < 100) {
        return github_com_mwitkow_go_proto_validators.FieldError("SomeInteger", fmt.Errorf(`value '%v' must be less than '100'`, this.SomeInteger))
    }
    if !(this.SomeFloat >= 0) {
        return github_com_mwitkow_go_proto_validators.FieldError("SomeFloat", fmt.Errorf(`value '%v' must be greater than or equal to '0'`, this.SomeFloat))
    }
    if !(this.SomeFloat <= 1) {
        return github_com_mwitkow_go_proto_validators.FieldError("SomeFloat", fmt.Errorf(`value '%v' must be lower than or equal to '1'`, this.SomeFloat))
    }
    return nil
}

安装go-grpc-middleware

go get github.com/grpc-ecosystem/go-grpc-middleware 

启动服务端

type SimpleVServer struct {
    pbv.UnimplementedSimpleVServerServer
}

func (s *SimpleVServer) TestSimpleV(ctx context.Context, in *pbv.InnerMessage) (resp *pbv.OuterMessage, err error) {
    resp = new(pbv.OuterMessage)

    fmt.Println(in.SomeFloat)
    fmt.Println(in.SomeInteger)

    return
}

func main() {
    s := grpc.NewServer(
        grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
            grpc_validator.StreamServerInterceptor(),
        )),
        grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
            grpc_validator.UnaryServerInterceptor(),
        )),
    )
    pbv.RegisterSimpleVServerServer(s, &SimpleVServer{})

    lis, err := net.Listen("tcp", ":1015")
    if err != nil {
        panic(fmt.Errorf("[App-Initialize-Error] GRPC服务Listen失败: %s \n", err))
    }

    if err = s.Serve(lis); err != nil {
        panic(fmt.Errorf("[App-Initialize-Error] GRPC服务启动失败: %s \n", err))
    }

    fmt.Println("SimpleV main")
}

启动客户端

func main() {
    conn, err := grpc.Dial(
        ":1015",
        grpc.WithTransportCredentials(insecure.NewCredentials()),
    )
    if err != nil {
        panic(fmt.Errorf("did not connect: %v", err))
    }
    defer conn.Close()

    c := pbv.NewSimpleVServerClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
    defer cancel()

    r, err2 := c.TestSimpleV(ctx, &pbv.InnerMessage{SomeInteger: 111, SomeFloat: 20.2})
    if err2 != nil {
        fmt.Println("err", "c.TestSimple.err", err2)
        return
    }

    fmt.Println(r)
}

请求参数SomeInteger值为111,此时启动服务端、客户端,客户端会看到以下输出

rpc error: code = InvalidArgument desc = invalid field SomeInteger: value '111' must be less than '100'


常见的验证规则

数字类型

  1. 参数必须大于某个数

    int64 id = [(validate.rules).int64 = {gt: 0}];
  2. 参数必须某个区间内

    int32 age = [(validate.rules).int64 = {gt:0, lte: 100}];
  3. 参数in某些值内

    uint32 status = [(validate.rules).uint32 = {in: [1,2,3]}];
  4. 参数不能in某些值内

    float money = [(validate.rules).float = {not_in: [0, 3.1415926]}];

布尔类型

  1. 参数必须为 true

    bool is_show = [(validate.rules).bool.const = true];
  2. 参数必须为 false

    bool is_show = [(validate.rules).bool.const = false];

文本类型

  1. 参数必须是某个值

    string name = [(validate.rules).string.const = "zhangsan"];
  2. 参数长度固定

    string phone = [(validate.rules).string.len = 11];
  3. 参数最小长度

    string password = [(validate.rules).string.min_len =  10];
  4. 参数最小最大长度

    string password = [(validate.rules).string = {min_len: 10, max_len: 20}];
  5. 参数正则校验

    string card = [(validate.rules).string.pattern = "(?i)^[0-9a-f]+$"];
  6. 参数必须是 email 格式

    string email = [(validate.rules).string.email = true];


答疑解惑

  • GoLand中 import "github.com/mwitkow/go-proto-validators@v0.3.2/validator.proto" 以及所有的 validator,报错Cannot resolve import

解决方案:

  1. 进入设置:GoLand -> Settings -> Preferences | Languages & Frameworks > Protocol Buffers
  2. 取消 Configure automatically的勾选,并选择+号
  3. 找到自己电脑上 go mod所在的目录,保存即可

goland

如果执行完这一步还是红色提示,那么需要检查该包是否在go mod目录下,并且版本号、路径也需要保持一致


总结

本文介绍了如何使用gprc_middleware中的validator来实现gRPC的请求参数校验,grpc_middleware 还提供了grpc_zap 、grpc_auth 、grpc_recovery 、grpc_ratelimit等,更多更详细使用点击 go-grpc-middleware 查看。

当然如果我们不熟悉这一套验证规则,但是非常熟悉Gin中go-playground-validator验证器,那么也可以思考一下我们该如何实现这种验证方式呢?(后续也会出一篇文章介绍如何实现这种方式)。

好了,这篇文章到此就结束了,希望能帮助你解决你所遇到的问题。



END

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

受教了,谢谢大佬

3个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!