跳转到主要内容

在选择您希望如何公开您的 Go 服务时,在 gRPC 和 HTTP 之间进行选择可能是一个相当困难的选择。 gRPC 为您提供了更高性能的网络传输,而围绕它的工具仍然落后于您更标准的基于 HTTP 的 API。

在本教程中,我们将研究如何利用 go-playground/validator 包来提高 API 和 UX 组件的安全性。

gRPC 服务中的验证


我喜欢 gRPC 服务开发的关键之一是通过 protobuf 定义对服务消费者施加的严格性。获得此设置的工具可以说更复杂一些,并且需要更长的时间,但一旦到位,它有助于确保消费者向您发送您期望的有效负载。

HTTP 替代方案


不过值得庆幸的是,有一种方法可以对传入 Go 服务的 HTTP JSON 请求实施某种程度的严格性。我们可以在我们的 HTTP 服务中使用一种称为 JSON 请求验证的技术,它可以有效地确保传入的请求在我们的服务处理之前通过一系列检查。

一个简单的例子


例如,让我们看一下处理诸如配置新用户帐户之类的任务的用户管理系统。我们通常会有一个看起来有点像这样的端点来创建新用户:

HTTP POST /api/v1/user
Content-Type application/json

{"username": "elliot", "email": "support@tutorialedge.net"}

我们传输层中的代码通常会接收这个 HTTP 请求的主体,并尝试将其解组为如下结构:

type User struct {
    Username string `json:"username"`
    Email string `json:"email"`
}

// server setup code

// PostUser - handles the provisioning of a new user account
func (h *Handler) PostUser(w http.ResponseWriter, r *http.Request) {
    var u User
    // ignoring errors is bad - this is just an example
    _ = json.Unmarshal([]byte(request.Body), &u)

    newUser, err := h.UserService.CreateUser(r.Context(), u)
    // handle any errors 
    // send responses back to the caller over the ResponseWriter 
}

现在这个流程可以工作了,但是我们可以改进它并在这个处理函数中添加验证规则,在我们将请求正文解组到我们的结构中之后,它会包含我们期望的所有字段。

事实上,我们不仅可以验证这些字段是否存在(如果需要),我们还可以对这些字段的内容进行额外的验证,并确保例如电子邮件字段实际上是有效的电子邮件。

使用 go-playground/validator 添加验证

GitHub Package Link - go-playground/validator

让我们扩展上面的示例代码以包含我们讨论的电子邮件验证。使用诸如 go-playground/validator 之类的包,我们可以在结构上添加额外的标签信息,并在我们解组后调用 validate:

import (
    // other imports
    "github.com/go-playground/validator/v10"
    //
)


type User struct {
    Username string `json:"username" validate:"required"`
    Email string `json:"email" validate:"email,required"`
}

// server setup code

// PostUser - handles the provisioning of a new user account
func (h *Handler) PostUser(w http.ResponseWriter, r *http.Request) {
    var u User
    _ = json.Unmarshal([]byte(request.Body), &u)


    validate := validator.New()
	err := validate.Struct(wh)
	if err != nil {
        // log out this error
        log.Error(err)
        // return a bad request and a helpful error message
        // if you wished, you could concat the validation error into this
        // message to help point your consumer in the right direction.
		http.Error(w, "failed to validate struct", 400)
        return
    }

    newUser, err := h.UserService.CreateUser(r.Context(), u)
}

在这个例子中,我们有效地实现了两件事:

  • 我们已经能够验证用户名和电子邮件字段都存在于未编组的 u User 结构中。
  • 我们在电子邮件字段中添加了验证,以尝试验证存在的任何字符串是否看起来像有效的电子邮件。

这有效地在我们的应用程序的入口点验证了我们的 API 的消费者不能向我们发送我们没有真正处理方式的数据。

其他类型的验证


现在,应该注意的是,我们不仅限于使用此包进行基本验证。这个项目的自述文件包含大量不同类型的验证,这些验证包含在包中,允许大量不同的用例。

如果您是构建基于基础设施的服务的网络工程师,那么您可以使用大量不同的基于网络的验证,例如 ipv4、ipv6、cidr、url 和 uri。

我们为什么要验证?


我们应该致力于在应用程序的入口点验证传入请求的最大原因之一是为了提高安全性。

使用这种方法,我们可以帮助保护服务的内部工作免受格式错误的数据或潜在的不良行为者将诸如 SQL 注入字符串之类的东西注入我们的服务中。

在这方面它当然不是灵丹妙药,您可能仍需要进一步提高应用程序的安全性,但这无疑是一个很好的开始。

这如何改善用户体验?


在开发 API 时,调用我们 API 的用户体验非常重要。如果您正在开发公共 API,情况尤其如此。

通过将此验证添加到我们的传输层,我们可以立即向 API 的使用者返回 400 Bad Request 状态,并通知他们缺少哪些字段,或者哪些字段可能未通过高级验证。

这使 API 的使用者可以立即了解给定请求失败的原因,然后采取措施解决任何问题。对于那些与返回诸如内部服务器错误或类似内容之类的无信息内容相反的用户而言,这是一种更好的用户体验。

更复杂的验证示例


应该注意的是,对于更复杂的验证用例,我们不限于已经包含在该包中的验证器列表。

如果您需要更复杂的验证,则可以选择编写自定义验证器并以与我们对烘焙品种所做的几乎相同的方式使用它们。

让我们看一下 repo 中给出的示例。它展示了我们如何定义一个相当随意的示例,但它表明我们对每个验证器实际检查的内容具有完全的灵活性。


   

   
package validators

import (
	"reflect"
	"strings"

	"github.com/go-playground/validator/v10"
)

// NotBlank is the validation function for validating if the current field
// has a value or length greater than zero, or is not a space only string.
func NotBlank(fl validator.FieldLevel) bool {
	field := fl.Field()

	switch field.Kind() {
	case reflect.String:
		return len(strings.TrimSpace(field.String())) > 0
	case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array:
		return field.Len() > 0
	case reflect.Ptr, reflect.Interface, reflect.Func:
		return !field.IsNil()
	default:
		return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
	}
}

此示例的测试文件显示了我们如何注册此验证器:

v := validator.New()
err := v.RegisterValidation("notblank", NotBlank)


结论


因此,在本文中,我们讨论了如何使用 go-playground/validator 包在应用程序中实现 HTTP JSON 请求验证。我们已经讨论了一些关键优势,以及它最终如何帮助提高系统的安全性。

如果您有兴趣在更能代表真实应用程序的代码中看到这种验证,那么我建议您查看我的最新课程 - 在 Go 中构建生产就绪服务 - 第 2 版

文章链接