Go Web 編程入門:驗證器

語言: CN / TW / HK

theme: devui-blue highlight: atom-one-dark


持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 27 天,點擊查看活動詳情

前言

網絡驗證可能是一個難題。 有句話在 Web 開發中流傳很廣的原則:

我們不能相信來自客户端用户表單的任何內容。

所以我們必須在使用這些數據之前驗證所有傳入數據。實現 REST API 是 Go 應用程序的典型用例。 API 接受的格式錯誤的數據可能會導致系統其他部分出現嚴重錯誤。

最好的情況是您的數據庫有一些機制來防止存儲格式錯誤的數據。如果不這樣做,這些數據可能會導致您面向客户的應用程序出現錯誤和意外行為(比如 SQL 注入)。

在這篇文章中,我們將介紹如何在 Go 中驗證發送到 REST API 的數據。

手動驗證輸入

簡易的 REST API

這是一個簡單的 REST API 示例,使用 gorilla/mux 包構建。它是一個很棒的 HTTP 路由器,特別是對於 REST API。 API 為一個端點提供路徑 /user。為簡單起見,它只接受所有用户的 HTTP GET 和創建用户的 HTTP Post。此外,它沒有持久性數據庫,而是用切片將用户存儲在內存中。

```go 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 函數來獲取字段的長度,以確保用户輸入了某些內容。

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

假設我們想在使用 Post 處理程序創建用户時根據需要設置 FirstName、LastName 和 Email。此外,我們希望電子郵件字段是有效的電子郵件地址。一種簡單的方法是手動驗證字段,如下所示:

```go 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()) } ```

完整示例:

```go 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 函數驗證郵箱中是否含有 @ 字符:

go 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 的有效性:

go 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 多個驗證器之下。

```go 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 包像這樣使用它:

首先我們定義一個驗證函數:

go 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 }

然後我們用驗證器實例註冊函數和相應的標籤。

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

現在我們在 User 結構的定義中添加標籤。

go 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 輸入對於防止應用程序出現格式錯誤的數據至關重要。您可以編寫自己的驗證邏輯,但在大多數情況下,最好使用維護良好的驗證包,例如上一節的推薦。

這使您可以在結構中使用標籤來配置驗證,並使運行驗證的邏輯保持簡單。如果您有一個需要不常見驗證功能的特殊用例,您仍然可以為驗證器包定義自己的擴展。

參考鏈接: