在 API 请求到来时,需要对请求进行一些通用的处理。Go 项目开发中,最常使用的通用处理是对请求进行认证鉴权、请求参数设置默认值和参数合法性校验。
本节课会介绍 miniblog 项目请求参数默认值设置和请求参数校验的具体实现方式。认证鉴权的实现会在本课程第 28 讲、第 29 讲中详细介绍。
提示:本节课最终源码位于 miniblog 项目的 feature/s23 分支。
添加 Bypass 认证中间件
Go 应用的后端需要根据租户信息,查询出所属租户的资源数据,查询方式类似于:
select * from post where userID='user-uvalgf';为了避免租户被伪造,从而越权查询出其他租户的数据,租户数据需要从认证 Token 中获取。Token 如果通过认证,说明其中的信息是真实、可信的。如图 10-1 所示。
图 10-1 租户获取
用户通过用户名和密码登录,后端会对请求中的明文密码进行加密,并将其与数据库中存储的该用户密码的加密字符串进行比较。如果匹配,说明用户输入的密码正确,登录成功。随后,后端根据用户名查询出该用户的信息(如 Username、UserID、Email 等),并使用这些关键数据签发 Token。
在后续的 API 请求中,前端会通过请求头携带该 Token,后端接收到请求后会验证 Token 的合法性。如果 Token 合法,说明其中包含的所有数据(如 UserID 等)是可信的。后端从 Token 中提取 UserID 等信息,并在查询数据时,通过 UserID 过滤出属于该用户的数据。
在开发了 Store 层、Biz 层、Handler 层代码之后,便可以对整个项目代码进行初步的测试,以尽快验证代码的设计是否合理、核心功能是否可用。因为租户 UserID 数据是从请求的 Token 中获取的,这时候整个项目还未实现认证功能,为了能够测试项目代码,可以开发一个 bypass 认证中间件,bypass 认证中间件会从请求头中获取用户的 UserID 数据,并放通所有请求。通过 bypass 中间件,既能够获取到租户数据,又能够让请求认证通过。
因为 miniblog 项目同时实现了 gRPC 服务器和 HTTP 服务器,所以需要分别为两类服务器开发并添加 bypass 认证中间件。
在 internal/pkg/middleware/grpc/bypass.go 文件中实现 gRPC 服务器的 bypass 中间件,代码实现如代码清单 9-11 所示。
代码清单 9-11 gRPC 服务器 Bypass 中间件实现
// AuthnBypasswInterceptor 是一个 gRPC 拦截器,模拟所有请求都通过认证。
func AuthnBypasswInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// 从请求头中获取用户ID
userID := "user-000001" // 默认用户ID
if md, ok := metadata.FromIncomingContext(ctx); ok {
// 获取header中指定的用户ID,假设Header名为"x-user-id"
if values := md.Get(known.XUserID); len(values) > 0 {
userID = values[0]
}
}
log.Debugw("Simulated authentication successful", "userID", userID)
// 将默认的用户信息存入上下文
//nolint: staticcheck
ctx = context.WithValue(ctx, known.XUserID, userID)
// 为 log 和 contextx 提供用户上下文支持
ctx = contextx.WithUserID(ctx, userID)
// 继续处理请求
return handler(ctx, req)
}
}代码清单 9-11 中,会从 gRPC 请求的 Header Metadata 中获取键为 x-user-id 的值,x-user-id 请求头保存了 UserID 的值。之后,将 UserID 数据保存在自定义上下文中,供后续的处理使用。
实现了 bypass 中间件之后,需要修改 internal/apiserver/grpcserver.go 文件,并添加以下代码,向 gRPC 拦截器链中添加 bypass 认证中间件:
...
func (c *ServerConfig) NewGRPCServerOr() (server.Server, error) {
// 配置 gRPC 服务器选项,包括拦截器链
serverOptions := []grpc.ServerOption{
// 注意拦截器顺序!
grpc.ChainUnaryInterceptor(
...
// Bypass 拦截器,通过所有请求的认证
mw.AuthnBypasswInterceptor(),
),
}
...
}HTTP 服务器的 bypass 中间件实现方式跟 gRPC 服务器的 bypass 中间件类似,请读者自行查看代码。
请求参数默认值设置
miniblog 项目请求参数默认值设置的实现借鉴了 Kubernetes API 接口请求参数默认值设置的实现思路:基于 API 接口定义文件自动生成默认值设置方法。
miniblog 项目使用 protoc-gen-go-defaults 项目提供的 protoc-gen-defaults 工具,基于 Protobuf 的扩展选项,来生成指定的默认值。
修改 pkg/api/apiserver/v1/user.proto 文件,给 CreateUserRequest 消息体的 nickname 字段添加[(defaults.value).string = "你好世界"] 扩展选项,来为 nickname 字段设置默认值。代码变更如下:
...
import "github.com/onexstack/defaults/defaults.proto";
...
// CreateUserRequest 表示创建用户请求
message CreateUserRequest {
// username 表示用户名称
string username = 1;
// password 表示用户密码
string password = 2;
// nickname 表示用户昵称
optional string nickname = 3 [(defaults.value).string = "你好世界"];
// email 表示用户电子邮箱
string email = 4;
// phone 表示用户手机号
string phone = 5;
}上述代码导入了 github.com/onexstack/defaults/defaults.proto 文件,需要将 onexstack/protobuf-go-plugins 项目仓库中 defaults 目录下的 Protobuf 文件拷贝并保存在 third_party/protobuf/github.com/onexstack/defaults/ 目录中。
修改 Makefile 的 protoc 规则,添加 protoc-gen-defaults 插件的调用配置,代码变更如下:
protoc: # 编译 protobuf 文件.
...
@protoc \
...
--defaults_out=paths=source_relative:$(APIROOT) \
$(shell find $(APIROOT) -name *.proto)
@find . -name "*.pb.go" -exec protoc-go-inject-tag -input={} \;在修改了 Makefile 规则后,执行以下命令,来为 CreateUserRequest 消息体生成默认值设置方法:
$ make protoc生成的代码位于 pkg/api/apiserver/v1/user.pb.defaults.go 文件中,生成的 Default() 方法代码如下:
func (x *CreateUserRequest) Default() {
if x.Nickname == nil {
v := string("你好世界")
x.Nickname = &v
}
}因为给请求添加默认值是所有请求都需要的通用操作,所以考虑通过 gRPC 拦截器来实现。为此,需要开发一个新的 gRPC 拦截器。新建 internal/pkg/middleware/grpc/defaulter.go 文件,代码如下:
// DefaulterInterceptor 是一个 gRPC 拦截器,用于对请求进行默认值设置.
func DefaulterInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, rq any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// 调用 Default() 方法(如果存在)
if defaulter, ok := rq.(interface{ Default() }); ok {
defaulter.Default()
}
// 继续处理请求
return handler(ctx, rq)
}
}上述代码,会判断 gRPC 请求的请求结构体是否实现了 Default() 方法,如果实现了就调用其 Default() 方法,为请求体中的字段设置默认值。实现了 DefaulterInterceptor 拦截器之后,还需要在 internal/apiserver/grpcserver.go 文件中添加 DefaulterInterceptor 拦截器。
HTTP 请求的请求参数默认值设置方法在本课程的第 25 讲中,已经介绍过了,本节课不再介绍。至此,请求参数默认值设置代码开发完成,完整代码见 feature/s21 分支。
小结(AI自动生成并人工审核)
本节课主要介绍了 Go 项目中常见的请求处理操作,包括请求认证绕过中间件(Bypass)、请求参数的默认值设置,以及请求参数校验的实现方式。为便于开发和测试,在认证鉴权未完全实现的情况下,可采用 Bypass 中间件模拟认证通过的功能。通过该中间件,能从请求头中提取用户的 UserID 并将其存储到上下文中,方便后续处理。gRPC 服务器通过实现拦截器来完成这一功能,而 HTTP 服务器则采用类似的中间件方案。
对于请求参数的默认值设置,miniblog 项目参考 Kubernetes 的实现方式,通过 Protobuf 的扩展选项定义字段默认值,并使用 protoc-gen-defaults 插件自动生成默认值设置代码。为使默认值的设置成为一个通用处理操作,gRPC 服务专门开发了一个 Defaulter 拦截器,通过调用消息体生成的 Default() 方法,为请求字段动态设置默认值,从而减少开发中的重复代码。
本文着重讲解了 gRPC 的实现细节,包括 Bypass 和 Defaulter 两个中间件的开发和作用,同时涉及对 Protobuf 文件和 Makefile 的配置修改,展示了实际开发中的最佳实践。HTTP 请求的默认值设置和认证相关内容将在后续课程中进行详细讲解。