Go Web 编程入门:验证器

举报
宇宙之一粟 发表于 2022/06/27 11:18:16 2022/06/27
【摘要】 前言网络验证可能是一个难题。 有句话在 Web 开发中流传很广的原则:我们不能相信来自客户端用户表单的任何内容。所以我们必须在使用这些数据之前验证所有传入数据。实现 REST API 是 Go 应用程序的典型用例。 API 接受的格式错误的数据可能会导致系统其他部分出现严重错误。最好的情况是您的数据库有一些机制来防止存储格式错误的数据。如果不这样做,这些数据可能会导致您面向客户的应用程序出现...

前言


网络验证可能是一个难题。 有句话在 Web 开发中流传很广的原则:


我们不能相信来自客户端用户表单的任何内容。


所以我们必须在使用这些数据之前验证所有传入数据。实现 REST API 是 Go 应用程序的典型用例。 API 接受的格式错误的数据可能会导致系统其他部分出现严重错误。


最好的情况是您的数据库有一些机制来防止存储格式错误的数据。如果不这样做,这些数据可能会导致您面向客户的应用程序出现错误和意外行为(比如 SQL 注入)。


在这篇文章中,我们将介绍如何在 Go 中验证发送到 REST API 的数据。


手动验证输入


简易的 REST API


这是一个简单的 REST API 示例,使用 gorilla/mux 包构建。它是一个很棒的 HTTP 路由器,特别是对于 REST API。 API 为一个端点提供路径 /user。为简单起见,它只接受所有用户的 HTTP GET 和创建用户的 HTTP Post。此外,它没有持久性数据库,而是用切片将用户存储在内存中。


package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strings"

    "github.com/gorilla/mux"
)

type User struct {
    ID                 int
    FirstName          string
    LastName           string
    FavouriteVideoGame string
    Email               string
}

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
    router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

    log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func validateEmail(email string) bool {
    // This is obviously not a good validation strategy for email addresses
    // pretend a complex regex here
    return !strings.Contains(email, "@")
}

func PostUser(w http.ResponseWriter, r *http.Request) {
    user := User{}
    json.NewDecoder(r.Body).Decode(&user)

    // We don't want an API user to set the ID manually
    // in a production use case this could be an automatically
    // ID in the database
    user.ID = id
    id++

    users = append(users, user)
    w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(users); err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}

现在让我们看看如何手动验证在请求正文中提供给此 API 的 POST 处理程序的输入。


手动验证输入


有时我们要求用户输入一些字段,但他们未能完成该字段。例如在上一节中,当我们需要用户名时。您可以使用 len 函数来获取字段的长度,以确保用户输入了某些内容。

    if len(r.Form["username"][0])==0{
        // code for empty field
    }


假设我们想在使用 Post 处理程序创建用户时根据需要设置 FirstName、LastName 和 Email。此外,我们希望电子邮件字段是有效的电子邮件地址。一种简单的方法是手动验证字段,如下所示:


if user.FirstName == "" {
   errs = append(errs, fmt.Errorf("Firstname is required").Error())
}

if user.LastName == "" {
   errs = append(errs, fmt.Errorf("LastName is required").Error())
}

if user.Email == "" || validateEmail(user.Email) {
   errs = append(errs, fmt.Errorf("A valid Email is required").Error())
}


完整示例:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/gorilla/mux"
)

type User struct {
    ID                 int
    FirstName          string
    LastName           string
    FavouriteVideoGame string
    Email               string
}

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
    router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

    log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func validateEmail(email string) bool {
    // That's obviously not a good validation strategy for email addresses
    // pretend a complex regex here
    return !strings.Contains(email, "@")
}

func PostUser(w http.ResponseWriter, r *http.Request) {
    user := User{}
    json.NewDecoder(r.Body).Decode(&user)

    errs := []string{}

    if user.FirstName == "" {
        errs = append(errs, fmt.Errorf("Firstname is required").Error())
    }

    if user.LastName == "" {
        errs = append(errs, fmt.Errorf("LastName is required").Error())
    }

    if user.Email == "" || validateEmail(user.Email) {
        errs = append(errs, fmt.Errorf("A valid Email is required").Error())
    }

    if len(errs) > 0 {
        w.Header().Add("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        if err := json.NewEncoder(w).Encode(errs); err != nil {
        }
        return
    }

    // We don't want an API user to set the ID manually
    // in a production use case this could be an automatically
    // ID in the database
    user.ID = id
    id++

    users = append(users, user)
    w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(users); err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}


可以看到这个验证方法非常冗长。我们必须定义一个自定义函数来验证常见的东西,比如电子邮件地址。让我们看看如何改进这一点。


上面只是简单通过 validateEmail 函数验证邮箱中是否含有 @ 字符:

func validateEmail(email string) bool {
    // That's obviously not a good validation strategy for email addresses
    // pretend a complex regex here
    return !strings.Contains(email, "@")
}



其实更好的方式是通过正则表达式来验证 E-mail 的有效性:

    if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
        fmt.Println("no")
    }else{
        fmt.Println("yes")
    }


使用结构标签验证输入


在 Go 中验证结构的一种更惯用的方法是使用结构标签。有许多通过结构标签进行结构验证的包。我们将在这里使用 https://github.com/go-playground/validator:该验证器基于标签实现结构和单个字段的值验证。


使用 go get github.com/go-playground/validator/v10 进行安装。


这不仅使我们能够使用结构标签进行验证,而且还提供了许多预定义的验证方法,例如电子邮件地址。


我们将对结构执行此验证,但不探讨如何填充结构。 我们可以假设数据将通过解析 JSON 有效负载、从表单输入显式填充或其他方法来填充。


如果您的数据需要其他验证器,请查看验证器包的文档。您需要的验证器很有可能在软件包提供的 80 多个验证器之下。


package main

import (
    "encoding/json"
    "log"
    "net/http"

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

type User struct {
    ID                 int    `validate:"isdefault"`
    FirstName          string `validate:"required"`
    LastName           string `validate:"required"`
    FavouriteVideoGame string
    Email               string `validate:"required,email"`
}

func main() {
    router := mux.NewRouter()
    router.HandleFunc("/user", PostUser).Methods(http.MethodPost)
    router.HandleFunc("/user", GetUsers).Methods(http.MethodGet)

    log.Fatal(http.ListenAndServe(":8081", router))
}

var users = []User{}
var id = 0

func PostUser(w http.ResponseWriter, r *http.Request) {
    user := User{}
    json.NewDecoder(r.Body).Decode(&user)

    validate := validator.New()

    err := validate.Struct(user)
    if err != nil {
        validationErrors := err.(validator.ValidationErrors)
        w.Header().Add("Content-Type", "application/json")
        w.WriteHeader(http.StatusBadRequest)
        responseBody := map[string]string{"error": validationErrors.Error()}
        if err := json.NewEncoder(w).Encode(responseBody); err != nil {
        }
        return
    }

    // We don't want an API user to set the ID manually
    // in a production use case this could be an automatically
    // ID in the database
    user.ID = id
    id++

    users = append(users, user)
    w.WriteHeader(http.StatusCreated)
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Content-Type", "application/json")
    if err := json.NewEncoder(w).Encode(users); err != nil {
        log.Println(err)
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
}


上面的 validationError.Error() 返回一个字符串,该字符串总结了结构中每个失败的验证。所以 BadRequest 响应仍然有非常详细的信息说明出了什么问题。


我们将验证更改为使用验证器包,现在根据以下规则进行验证:

  • ID 字段不应该由用户设置,所以我们验证它有 int 的默认值,即 0

  • FullName 和 LastName 是必需的

  • 电子邮件字段是必需的,并使用预定义的电子邮件验证器进行验证


使用自定义验证器验证输入


所以现在我们使用验证器包并使用结构标签验证结构。但是我们如何验证一个无法通过库提供的标签验证的结构字段呢?


假设我们想将某些视频游戏列入黑名单。因为我们不希望我们的系统中有喜欢 PUBG 或 Fortnite 等游戏的用户。在这种情况下,我们可以定义一个自定义的 validate 标记值并让 validate 包像这样使用它:


首先我们定义一个验证函数:


func GameBlacklistValidator(f1 validator.FieldLevel) bool {
    gameBlacklist := []string{"PUBG", "Fortnite"}
    game := f1.Field().String()
    for _, g := range gameBlacklist {
        if game == g {
            return false
        }
    }
    return true
}


然后我们用验证器实例注册函数和相应的标签。


...
    validate := validator.New()
    validate.RegisterValidation("game-blacklist", GameBlacklistValidator)
...


现在我们在 User 结构的定义中添加标签。

type User struct {
    ID                 int    `validate:"isdefault"`
    FirstName          string `validate:"required"`
    LastName           string `validate:"required"`
    FavouriteVideoGame string `validate:"game-blacklist"`
    Email              string `validate:"required,email"`
}


推荐验证库

Awesome Go 项目下有用于验证的库。这里推荐如下:



根据介绍赶紧为自己的项目挑一个吧~

总结


验证 REST API 输入对于防止应用程序出现格式错误的数据至关重要。您可以编写自己的验证逻辑,但在大多数情况下,最好使用维护良好的验证包,例如上面的推荐。


这使您可以在结构中使用标签来配置验证,并使运行验证的逻辑保持简单。如果您有一个需要不常见验证功能的特殊用例,您仍然可以为验证器包定义自己的扩展。


【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。