程序员社区

【Go Web开发】限制HTTP请求内容

在上一篇文章中为处理HTTP请求中的无效JSON内容和其他错误请求所做的优化是朝着正确方向迈出了一大步。但我们仍然可以做一些事情来使JSON处理更加健壮。

其中的一点就是处理HTTP请求中的未知字段。例如,下面的shell命令,向createMovieHandler接口发送一个包含未知字段rating的请求:

$ curl -i -d '{"title": "Moana", "rating":"PG"}' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:51:50 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8

{Title:Moana Year:0 Runtime:0 Genres:[]}

请注意这个请求是如何正常地工作的——没有通知客户端,应用程序碰到无法识别的rating字段。在某些场景中,默默忽略未知字段可能正是您想要的行为,但在这里,最好能够提醒客户端注意这个问题。

幸运的是json.Decoder提供了DisallowUnknownField()配制可以在碰到未知字段时报错。

另一个问题是json.Decoder支持JSON数据流。当我们在请求体上调用Decode()时,它实际上只从请求体读取第一个JSON值并解码。如果我们对Decode()进行第二次调用,它将读取并解码第二个JSON值,以此类推。

但是因为我们在readJSON()中只调用了一次Decode(),所以在请求体中第一个JSON值之后的任何内容都会被忽略。这意味着您可以发送一个包含多个JSON值的请求体,或者在第一个JSON值之后发送垃圾内容,而我们的API处理程序不会引发错误。例如:

# Body包含多个JSON值
$ curl -i -d '{"title": "Moana"}{"title": "Top Gun"}' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:53:57 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8

{Title:Moana Year:0 Runtime:0 Genres:[]}

# Body在第一个JSON值之后包含垃圾内容
$ curl -i -d '{"title": "Moana"} :~()' localhost:4000/v1/movies
HTTP/1.1 200 OK
Date: Tue, 06 Apr 2021 18:54:15 GMT Content-Length: 41
Content-Type: text/plain; charset=utf-8

{Title:Moana Year:0 Runtime:0 Genres:[]}

同样,这种行为可能非常有用,但在解析API请求时它并不适合。我们希望createMovieHandler处理程序的请求在请求体中只包含一个JSON对象,并只包含关于创建的电影的信息。

为了确保请求体中没有额外的JSON值(或任何其他内容),我们需要在readJSON()函数中第二次调用Decode(),并检查它是否返回一个io.EOF(文件结束)错误。

最后,目前我们对接受的请求体大小没有限制。这意味着对于任何企图对我们的API执行Dos攻击的恶意客户端来说,我们的createMovieHandler将是一个很脆弱的目标。我们可以通过使用http.MaxBytesReader()函数来限制请求体的最大值来解决这个问题。

让我们更新readJSON()函数来修复前面的三个问题:

File: cmd/api/helper.go



package main

...

func (app *application) readJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
    maxBytes := 1_048_576
        //使用http.MaxBytesReader函数限制只读取1MB以内的请求内容
    r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
    dec := json.NewDecoder(r.Body)
        //启用DisallowUnknownFields()函数,这意味着在解析JSON的过程中,如果有不可匹配的字段就返回错误。
    dec.DisallowUnknownFields()

    err := dec.Decode(dst)
    if err != nil {
        var syntaxError *json.SyntaxError
        var unmarshalTypeError *json.UnmarshalTypeError
        var invalidUnmarshalError *json.InvalidUnmarshalError
        switch {
        case errors.As(err, &syntaxError):
            return fmt.Errorf("body contains badly-formed JSON (at character %d)", syntaxError.Offset)
        case errors.Is(err, io.ErrUnexpectedEOF):
            return errors.New("body contains badly-formed JSON")
        case errors.As(err, &unmarshalTypeError):
            if unmarshalTypeError.Field != "" {
                return fmt.Errorf("body contains incorrect JSON type for field %q", unmarshalTypeError.Field)
            }
            return fmt.Errorf("body contain incorrect JSON type (at character %d)", unmarshalTypeError.Offset)
        case errors.Is(err, io.EOF):
            return errors.New("body must not be empty")
                //如果JSON中包含不能解析的字段,将返回“json: unknown field”错误,这里从错误内容中提取出字段名称
                //返回给客户端
        case strings.HasPrefix(err.Error(), "json: unknown field "):
            fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
            return fmt.Errorf("body contain unknown key %s", fieldName)
                //如果请求内容超过1MB将返回"http: request body too large"错误,返回给客户端
        case err.Error() == "http: request body too large":
            return fmt.Errorf("body must not be large than %d bytes", maxBytes)

        case errors.As(err, &invalidUnmarshalError):
            panic(err)
        default:
            return err
        }
    }
    err = dec.Decode(&struct{}{})
    if err != io.EOF {
        return errors.New("body must only contain a single JSON value")
    }
    return nil
}

修改完之后,我们再次尝试本章前面的请求:

$ curl -d '{"title": "Moana", "rating":"PG"}' localhost:4000/v1/movies
{
    "error": "body contains unknown key \"rating\""
}

$ curl -d '{"title": "Moana"}{"title": "Top Gun"}' localhost:4000/v1/movies
{
    "error": "body must only contain a single JSON value"
}

$ curl -d '{"title": "Moana"} :~()' localhost:4000/v1/movies
{
    "error": "body must only contain a single JSON value"
}

根据上面的结果发现请求处理被终止,客户端收到一个明确的错误消息,解释了错误原因。

下面可以发送一个包含大数据量的请求,看看响应结果。

为了演示,作者创建了一个1.5MB的JSON文件,您可以通过运行以下命令将其下载到/tmp目录:

$ wget -O /tmp/largefile.json https://www.alexedwards.net/static/largefile.json

如果你将这个文件内容作为请求体发送到POST /v1/movies接口,http.MaxByteReader()会检查并报错,你将收到如下响应信息:

$ curl -d @/tmp/largefile.json localhost:4000/v1/movies
{
    "error": "body must not be larger than 1048576 bytes"
}

完成以上修改,我们总算完成了readJSON()函数的业务处理。

必须承认readJSON()里面的代码并不是最漂亮的…我们引入了许多错误处理和逻辑,用于对Decode()函数返回错误的处理。但现在已经写好了。您不需要再修改它,而且您可以轻松地将它复制和粘贴到其他项目中。

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 【Go Web开发】限制HTTP请求内容

一个分享Java & Python知识的社区