美文网首页
【Go Web开发】校验JSON内容

【Go Web开发】校验JSON内容

作者: Go语言由浅入深 | 来源:发表于2022-01-26 00:24 被阅读0次

    备注:【Go Web开发】是一个从零开始创建关于电影管理的Web项目。

    在许多情况下,您需要对来自客户端的数据执行额外的验证或检查,以确保它在处理之前满足特定的业务规则。在本文中,我们将通过更新createMovieHandler来演示如何在JSON API的上下文中做到这一点:

    • 客户端提供的movie标题不为空,长度不超过500字节。
    • movie的year字段不能是空的,而且是在1888年到今年之间。
    • runtime字段不能空,而且是一个正数。
    • genres字段包含1-5个不同的电影类型。

    如果其中任何一个检查失败,我们希望向客户端发送一个422 Unprocessable Entity响应,以及清楚地描述验证失败的错误消息。

    创建validator包

    为了在整个项目中帮助我们进行验证,将创建一个internal/validator包,其中包含一些简单的可重用的帮助类型和函数。如果您正在跟随本文操作,请在您的机器上创建以下目录和文件:

    $ mkdir internal/validator
    $ touch internal/validator/validator.go
    

    然后在文件internal/validator/validator.go中添加如下代码:

    package validator
    
    import "regexp"
    
    var (
            //申明一个正则表达式用于检查email的格式,如果你有兴趣该正则表达式来自于“https://html.spec.whatwg.org/#valid-e-mail-address”网站。
        EmailRx = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\. [a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
    )
    
    //定义一个新的Validator类型,其中包含验证错误的map。
    type Validator struct {
        Errors map[string]string
    }
    
    //New是一个构造函数,用于创建Validator实例
    func New() *Validator {
        return &Validator{Errors: make(map[string]string)}
    }
    
    //valid 返回true如果map中没有错误
    func (v *Validator) Valid() bool {
        return len(v.Errors) == 0
    }
    
    //AddError 向map中添加错误(map中不存在对应key的错误)
    func (v *Validator) AddError(key, message string) {
        if _, exists := v.Errors[key]; !exists {
            v.Errors[key] = message
        }
    }
    
    //Check 向map中添加错误消息,如果校验失败即ok为false
    func (v *Validator) Check(ok bool, key, message string) {
        if !ok {
            v.AddError(key, message)
        }
    }
    
    //In 如果list切片中存在value字符串返回true
    func In(value string, list ...string) bool {
        for i := range list {
            if value == list[i] {
                return true
            }
        }
        return false
    }
    
    //Match 如果字符串满足正则表达式就返回true
    func Matches(value string, rx *regexp.Regexp) bool {
        return rx.MatchString(value)
    }
    
    //如果切片中的字符串都不同返回true
    func Unique(values []string) bool {
        uniqueValues := make(map[string]bool)
        for _, value := range values {
            uniqueValues[value] = true
        }
        return len(values) == len(uniqueValues)
    }
    
    

    总结:

    在上面的代码中定义了Validator类型,包含一个存储错误信息map字段。Validator提供了Check()方法,根据校验结果向map中添加错误信息,而Valid()方法返回map是否包含错误信息。还添加了In(), Matches()和Unique()方法来帮助我们执行特定字段的检查。

    从概念上讲,这个Validator类型是非常简单的,但这并不是一件坏事。正如我们将在其他地方看到的,它在开发中功能强大,为我们提供了很多灵活性的字段检查。

    执行字段检查

    下面我们把validator类型使用起来。

    我们需要做的第一件事是更新cmd/api/errors.go文件,添加一个新的failedValidationResponse()帮助函数,它将写入一个422 Unprocessable Entity错误码,并将来自新Validator类型的错误内容映射为JSON响应体。

    File: cmd/api/errors.go


    package main
    
    ...
    
    //注意errors参数是一个map类型,和validator类型包含map一致
    func (app *application) failedValidationResponse(w http.ResponseWriter, r *http.Request, errors map[string]string) {
        app.errorResponse(w, r, http.StatusUnprocessableEntity, errors)
    }
    

    完成之后,返回到createMovieHandler并更新它,以对input结构体各个字段进行必要的检查。像这样:

    File:cmd/api/movies.go


    package main
    
    import (
        "fmt"
        "net/http"
        "time"
    
        "greenlight.alexedwards.net/internal/data"
        "greenlight.alexedwards.net/internal/validator"
    )
    
    func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Title   string       `json:"title"`
            Year    int32        `json:"year"`
            Runtime data.Runtime `json:"runtime"`
            Genres  []string     `json:"genres"`
        }
        err := app.readJSON(w, r, &input)
        if err != nil {
            app.badRequestResponse(w, r, err)
            return
        }
        movie := &data.Movie{
            Title:   input.Title,
            Year:    input.Year,
            Runtime: input.Runtime,
            Genres:  input.Genres,
        }
            //初始化一个新的Validator实例
        v := validator.New()
      
        //使用Check()方法执行字段校验。如果校验失败就会向map中添加错误信息。例如下面第一行检查title不能为空,然后再检查长度不能超过500字节等等。
        v.Check(movie.Title != "", "title", "must be provide")
        v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long")
    
        v.Check(movie.Year != 0, "year", "must be provided")
        v.Check(movie.Year >= 1888, "year", "must be greater than 1888")
        v.Check(movie.Year <= int32(time.Now().Year()), "year", "must not be in the future")
    
        v.Check(movie.Runtime != 0, "runtime", "must be provided")
        v.Check(movie.Runtime > 0, "runtime", "must be a positive integer")
    
        v.Check(movie != nil, "genres", "must be provided")
        v.Check(len(movie.Genres) >= 1, "genres", "must contain at least 1 genres")
        v.Check(len(movie.Genres) <= 5, "genres", "must not contain more than 5 genres")
        //使用Unique()方法,检查input.Genres每个字段是否唯一。
        v.Check(validator.Unique(movie.Genres), "genres", "must not contain duplicate values")
    
        //使用Valid()方法确认检查是否通过。如果有错误就使用failedValidationResponse()帮助函数返回错误信息给客户端。
       if !v.Valid() {
            app.failedValidationResponse(w, r, v.Errors)
            return
        }
        fmt.Fprintf(w, "%+v\n", input)
    }
    

    做完这个之后,我们就可以试一下了。重新启动服务,然后向post /v1/movie接口发送请求,其中包含一些不合法的字段信息,类似下面:

    $ BODY='{"title":"","year":1000,"runtime":"-123 mins","genres":["sci-fi","sci-fi"]}' $ curl -i -d "$BODY" localhost:4000/v1/movies
    HTTP/1.1 422 Unprocessable Entity
    Content-Type: application/json
    Date: Wed, 07 Apr 2021 10:33:57 GMT 
    Content-Length: 180
    
    {
        "error":
        {
            "genres": "must not contain duplicate values",
            "runtime": "must be a positive integer",
            "title": "must be provided",
            "year": "must be greater than 1888"
        }
    }
    

    看起来不错。我们的检查功能生效了,并阻止请求被执行—甚至更好的是,向客户端返回一个格式良好的JSON响应,其中包含针对每个检验错误的详细信息。

    你也可以发送正常的请求体,你会发现请求被正常处理,input内容在响应中返回给客户端:

    $ BODY='{"title":"Moana","year":2016,"runtime":"107 mins","genres":["animation","adventure"]}'
    $ curl -i -d "$BODY" localhost:4000/v1/movies
    HTTP/1.1 200 OK
    Date: Tue, 23 Nov 2021 12:33:45 GMT
    Content-Length: 65
    Content-Type: text/plain; charset=utf-8
    
    {Title:Moana Year:2016 Runtime:107 Genres:[animation adventure]}
    
    

    使校验规则可重用

    在大型项目中,很多个接口需要重复这种校验的过程,因此将上面的校验规则抽象成方法供其他地方使用。比如客户端要更新movie也会传一些新的字段内容,也需要校验。

    避免重复,我们可以将movie的校验整合到一个单独的ValidateMovie()函数中去。理论上,这个函数可以放在任意位置。但就个人而言,我喜欢将验证检查放在internal/data包中的相关领域类型附近。

    如果按照下面的步骤操作,请重新打开internal/data/movies.go然后添加一个ValidateMovie()函数,其中包含如下检查:

    File: internal/data/movies.go


    package data
    
    import (
        "encoding/json"
        "fmt"
        "greenlight.alexedwards.net/internal/validator"
        "time"
    )
    
    type Movie struct {
        ID       int64     `json:"id"`
        CreateAt time.Time `json:"-"`
        Title    string    `json:"title"`
        Year     int32     `json:"year,omitempty"`
        Runtime  Runtime   `json:"runtime,omitempty,string"`
        Genres   []string  `json:"genres,omitempty"`
        Version  int32     `json:"version"`
    }
    
    func ValidateMovie(v *validator.Validator, movie *Movie) {
        v.Check(movie.Title != "", "title", "must be provide")
        v.Check(len(movie.Title) <= 500, "title", "must not be more than 500 bytes long")
    
        v.Check(movie.Year != 0, "year", "must be provided")
        v.Check(movie.Year >= 1888, "year", "must be greater than 1888")
        v.Check(movie.Year <= int32(time.Now().Year()), "year", "must not be in the future")
    
        v.Check(movie.Runtime != 0, "runtime", "must be provided")
        v.Check(movie.Runtime > 0, "runtime", "must be a positive integer")
    
        v.Check(movie != nil, "genres", "must be provided")
        v.Check(len(movie.Genres) >= 1, "genres", "must contain at least 1 genres")
        v.Check(len(movie.Genres) <= 5, "genres", "must not contain more than 5 genres")
        v.Check(validator.Unique(movie.Genres), "genres", "must not contain duplicate values")
    }
    

    重要提示:现在检查是对一个movie结构体实例各个字段进行的,而不是对input结构体。

    完成上面的改造之后,我们需要返回createMovieHandler并更新代码,通过初始化一个新的Movie结构体,从input结构体复制数据到movie结构体中,然后调用这个新的验证函数。像这样:

    File:cmd/api/movies.go


    package main
    
    ...
    
    func (app *application) createMovieHandler(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Title   string       `json:"title"`
            Year    int32        `json:"year"`
            Runtime data.Runtime `json:"runtime"`
            Genres  []string     `json:"genres"`
        }
        err := app.readJSON(w, r, &input)
        if err != nil {
            app.badRequestResponse(w, r, err)
            return
        }
        movie := &data.Movie{
            Title:   input.Title,
            Year:    input.Year,
            Runtime: input.Runtime,
            Genres:  input.Genres,
        }
    
            //初始化Validator实例
        v := validator.New()
    
        //调用ValidateMovie()函数,如果有错误就返回给客户端。
        if data.ValidateMovie(v, movie); !v.Valid() {
            app.failedValidationResponse(w, r, v.Errors)
            return
        }
        fmt.Fprintf(w, "%+v\n", input)
    }
    

    当您查看这些代码时,您的脑海中可能会有几个问题。

    首先,您可能想知道为什么我们在处理程序中初始化Validator实例并将其传递给ValidateMovie()函数——而不是在ValidateMovie()中初始化它并将其作为返回值传递回来。

    这是因为随着应用程序变得越来越复杂,我们将需要从处理程序调用多个校验帮助函数,而不是像上面所示的就一个。因此,在处理程序中初始化Validator,然后传递给帮助函数,这给了我们更多的灵活性。

    您可能还想知道,为什么我们要将JSON请求解码为input结构体类型,然后复制数据,而不是直接解码为Movie结构体实例。

    因为movie里面有些字段例如ID和Version是不需要客户端提供的,如果使用movie的话,客户端提供ID和Verison字段也会被解码到movie结构体中,这就需要多余的检查工作。

    但是将客户端的请求内容解析到一个临时的结构体中,会更灵活,简洁而且代码更健壮。

    有了这些解释,您应该能够再次启动应用程序,并且从客户端的角度来看,效果应该与之前的一样。如果你发起一个无效的请求,你应该会得到一个包含类似这样的错误消息的响应:

    $ BODY='{"title":"","year":1000,"runtime":"-123 mins","genres":["sci-fi","sci-fi"]}' 
    $ curl -i -d "$BODY" localhost:4000/v1/movies
    HTTP/1.1 422 Unprocessable Entity
    Content-Type: application/json
    Date: Wed, 07 Apr 2021 10:33:57 GMT 
    Content-Length: 180
    
    {
        "error":
        {
            "genres": "must not contain duplicate values",
            "runtime": "must be a positive integer",
            "title": "must be provided",
            "year": "must be greater than 1888"
        }
    }
    

    您可以随意测试,并尝试在JSON中发送不同的值,直到所有的校验都按预期工作为止。

    相关文章

      网友评论

          本文标题:【Go Web开发】校验JSON内容

          本文链接:https://www.haomeiwen.com/subject/ugvghrtx.html