跳到主要内容

04-参数校验中间件

参数校验

Validate 中间件使用 proto-gen-validate 生成后的代码进行参数校验,我们可以通过在 proto 中编写参数校验规则,然后生成代码,通过中间件自动的进行校验。

安装

在使用 validate 之前首先需要安装 proto-gen-validate。

go install github.com/envoyproxy/protoc-gen-validate@latest

如果使用中遇到无法使用或者生成的代码中 包含 // no validation rules for xxxx

可以尝试 git clone github.com/envoyproxy/protoc-gen-validate 然后 make build

写法

syntax = "proto3";

package examplepb;

import "validate/validate.proto";

message Person {
uint64 id = 1 [(validate.rules).uint64.gt = 999];

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

string name = 3 [(validate.rules).string = {
pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",
max_bytes: 256,
}];

Location home = 4 [(validate.rules).message.required = true];

message Location {
double lat = 1 [(validate.rules).double = {gte: -90, lte: 90}];
double lng = 2 [(validate.rules).double = {gte: -180, lte: 180}];
}
}

我们上节课讲了如何注册用户,但是如果用户传了个空用户名,我们也会将其保存到数据库,这显然是不太合理的,我们需要给我们的前端请求参数加上校验规则,比如用户名非空,年龄大于0,笔者带大家一步步实现:

  1. 在agent.proto 文件中找到 message CreateUserRequest ,并修改如下:
message CreateUserRequest {
string name = 1 [(validate.rules).string.min_len = 1];
int32 age = 2 [(validate.rules).int64 = {gt: 0}];
}
  1. 在Makefile中添加validate命令
.PHONY: validate
# generate validate proto
validate:
protoc --proto_path=. \
--proto_path=./third_party \
--go_out=paths=source_relative:. \
--validate_out=paths=source_relative,lang=go:. \
$(API_PROTO_FILES)
  1. 执行命令
make validate
  1. 确认文件生成路径正确,需要保持同一目录

  1. 中间件验证参数

kratos 框架默认给我们提供了 validate 中间件,在中间件中会判断 request 对象是否实现了 validator 接口 ,如下:

type validator interface {
Validate() error
}

// Validator is a validator middleware.
func Validator() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
if v, ok := req.(validator); ok {
if err := v.Validate(); err != nil {
return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err)
}
}
return handler(ctx, req)
}
}
}

所以上一步,我们生成的 agent.pb.validate.go 需要与 agent.pb.go 同级目录,才能将 validate 方法绑定在 CreateUserRequest 这个结构体上:

// agent.pb.validate.go 文件实现接口

// Validate checks the field values on CreateUserRequest with the rules defined
// in the proto definition for this message. If any rules are violated, the
// first error encountered is returned, or nil if there are no violations.
func (m *CreateUserRequest) Validate() error {
return m.validate(false)
}

我们可以将 validate 中间件注入到 http 或者 grpc 中,在有请求进入时 validate 中间件会自动对参数根据 proto 中编写的规则进行校验。

http

httpSrv := http.NewServer(
http.Address(":8000"),
http.Middleware(
validate.Validator(),
))

grpc

grpcSrv := grpc.NewServer(
grpc.Address(":9000"),
grpc.Middleware(
validate.Validator(),
))

References

中间件

Kratos 内置了一系列的 middleware(中间件)用于处理 logging、 metrics 等通用场景。您也可以通过实现 Middleware 接口,开发自定义 middleware,进行通用的业务处理,比如用户登录鉴权等。

内置中间件

相关代码均可以在 middleware 目录下找到。

  • logging: 用于请求日志的记录。
  • metrics: 用于启用 metric。
  • recovery: 用于 recovery panic。
  • tracing: 用于启用 trace。
  • validate: 用于处理参数校验。
  • metadata: 用于启用元信息传递。
  • auth: 用于提供基于 JWT 的认证请求。
  • ratelimit: 用于服务端流量限制。
  • circuitbreaker: 用于客户端熔断控制。

生效顺序

一个请求进入时的处理顺序为 Middleware 注册的顺序,而响应返回的处理顺序为注册顺序的倒序,即先进后出(FILO)。

         ┌───────────────────┐
│MIDDLEWARE 1
│ ┌────────────────┐│
│ │MIDDLEWARE 2 ││
│ │ ┌─────────────┐││
│ │ │MIDDLEWARE 3 │││
│ │ │ ┌─────────┐ │││
REQUEST │ │ │ │ YOUR │ │││ RESPONSE
──────┼─┼─┼─▷ HANDLER ○─┼┼┼───▷
│ │ │ └─────────┘ │││
│ │ └─────────────┘││
│ └────────────────┘│
└───────────────────┘

使用中间件

在 NewGRPCServer 和 NewHTTPServer 中通过 ServerOption 进行注册。例如:

// http
// 定义opts
var opts = []http.ServerOption{
http.Middleware(
recovery.Recovery(), // 把middleware按照需要的顺序加入
tracing.Server(),
logging.Server(),
),
}

// 创建server
http.NewServer(opts...)

//grpc
var opts = []grpc.ServerOption{
grpc.Middleware(
recovery.Recovery(), // 把middleware按照需要的顺序加入
tracing.Server(),
logging.Server(),
),
}

// 创建server
grpc.NewServer(opts...)

自定义中间件

需要实现 Middleware 接口。

中间件中您可以使用 tr, ok := transport.FromServerContext(ctx) 获得 Transporter 实例以便访问接口相关的元信息。

基本的代码模板:

import (
"context"

"github.com/go-kratos/kratos/v2/middleware"
"github.com/go-kratos/kratos/v2/transport"
)

func Middleware1() middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
if tr, ok := transport.FromServerContext(ctx); ok {
// Do something on entering
defer func() {
// Do something on exiting
}()
}
return handler(ctx, req)
}
}
}

定制中间件

对特定路由定制中间件:

  • server: selector.Server(ms...)
  • client: selector.Client(ms...)

匹配规则(多参数):

  • Path(path...): 路由匹配
  • Regex(regex...): 正则匹配
  • Prefix(prefix...): 前缀匹配
  • Match(fn): 函数匹配,函数格式为func(ctx context.Context,operation string) booloperation为 path,函数返回值为true,匹配成功,ctx可使用transport.FromServerContext(ctx) 或者 transport.FromClientContext(ctx获取 Transporter)

http server

import "github.com/go-kratos/kratos/v2/middleware/selector"

http.Middleware(
selector.Server(recovery.Recovery(), tracing.Server(),testMiddleware).
Path("/hello.Update/UpdateUser", "/hello.kratos/SayHello").
Regex(`/test.hello/Get[0-9]+`).
Prefix("/kratos.", "/go-kratos.", "/helloworld.Greeter/").
Build(),
)

http client

import "github.com/go-kratos/kratos/v2/middleware/selector"

http.WithMiddleware(
selector.Client(recovery.Recovery(), tracing.Server(),testMiddleware).
Path("/hello.Update/UpdateUser", "/hello.kratos/SayHello").
Regex(`/test.hello/Get[0-9]+`).
Prefix("/kratos.", "/go-kratos.", "/helloworld.Greeter/").
Match(func(ctx context.Context,operation string) bool {
if strings.HasPrefix(operation, "/go-kratos.dev") || strings.HasSuffix(operation, "world") {
return true
}
tr, ok := transport.FromClientContext(ctx)
if !ok {
return false
}
if tr.RequestHeader().Get("go-kratos") == "kratos" {
return true
}
return false
}).Build(),
)

grpc server

import "github.com/go-kratos/kratos/v2/middleware/selector"

grpc.Middleware(
selector.Server(recovery.Recovery(), tracing.Server(),testMiddleware).
Path("/hello.Update/UpdateUser", "/hello.kratos/SayHello").
Regex(`/test.hello/Get[0-9]+`).
Prefix("/kratos.", "/go-kratos.", "/helloworld.Greeter/").
Build(),
)

grpc client

import "github.com/go-kratos/kratos/v2/middleware/selector"

grpc.Middleware(
selector.Client(recovery.Recovery(), tracing.Server(),testMiddleware).
Path("/hello.Update/UpdateUser", "/hello.kratos/SayHello").
Regex(`/test.hello/Get[0-9]+`).
Prefix("/kratos.", "/go-kratos.", "/helloworld.Greeter/").
Build(),
)

注意: 定制中间件是通过 operation 匹配,并不是 http 本身的路由!!!

operation 是 HTTP 及 gRPC 统一的 gRPC path。

operation 查找

gRPC path 的拼接规则为 /包名.服务名/方法名(/package.Service/Method)

比如在如下 proto 文件中,我们要调用 SayHello 这个方法,那么 operation 就为 /helloworld.Greeter/SayHello

syntax = "proto3";

package helloworld;

import "google/api/annotations.proto";

option go_package = "github.com/go-kratos/examples/helloworld/helloworld";

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
get: "/helloworld/{name}",
};
}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings
message HelloReply {
string message = 1;
}