Docs Vault

在 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 请求的默认值设置和认证相关内容将在后续课程中进行详细讲解。