Golang解析请求体时抛出异常,包含未知字段”password”。

huangapple go评论109阅读模式
英文:

Golang Parsing Request body throws, contains unknown field "password"?

问题

我不确定是什么原因导致这个问题,但它抛出异常的原因是当我调用dec.DisallowUnknownFields()时,它会认为密码不是User结构体/请求体中的一个字段。尽管密码的JSON值是“-”。所以我最初想将JSON中的字段名改为“password”,但是这会抛出一个错误:

internal server error: illegal base64 data at input byte 4

Postman中的请求体:

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "george@gmail.com",
    "phone": "703-123-4567",
    "password": "abc12"
}

我还尝试将密码的类型更改为字符串,但也没有起作用,但可以说这是错误的解决方案,因为我们应该将密码存储为哈希值在数据库中。所以,目前我已经没有其他地方可以寻求解决这个问题的原因了...

models.go

type User struct {
	ID primitive.ObjectID    `json:"_id,omitempty" bson:"_id,omitempty"`
	FirstName      string    `json:"firstname"`
	LastName       string    `json:"lastname"`
	Email          string    `json:"email"`
	Phone 		   string    `json:"phone"`
	Password 	   []byte    `json:"-"`
	Created        time.Time `json:"-"`
	Active         bool      `json:"-"`
	Address        Address   `json:"address,omitempty"`
}

Helpers.go

type malformedRequest struct {
	status int
	msg    string
}

func (mr *malformedRequest) Error() string {
	return mr.msg
}

func (app *appInjection) decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
	if r.Header.Get("Content-Type") != "" {
		value, _ := header.ParseValueAndParams(r.Header, "Content-Type")
		if value != "application/json" {
			msg := "Content-Type header is not application/json"
			return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg}
		}
	}

	r.Body = http.MaxBytesReader(w, r.Body, 1048576)

	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	err := dec.Decode(&dst)

	if err != nil {
		var syntaxError *json.SyntaxError
		var unmarshalTypeError *json.UnmarshalTypeError

		switch {
		case errors.As(err, &syntaxError):
			msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.Is(err, io.ErrUnexpectedEOF):
			msg := fmt.Sprintf("Request body contains badly-formed JSON")
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.As(err, &unmarshalTypeError):
			msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case strings.HasPrefix(err.Error(), "json: unknown field "):
			fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
			msg := fmt.Sprintf("Request body contains unknown field %s", fieldName)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.Is(err, io.EOF):
			msg := "Request body must not be empty"
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case err.Error() == "http: request body too large":
			msg := "Request body must not be larger than 1MB"
			return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: msg}

		default:
			return err
		}
	}

	err = dec.Decode(&struct{}{})
	if err != io.EOF {
		msg := "Request body must only contain a single JSON object"
		return &malformedRequest{status: http.StatusBadRequest, msg: msg}
	}

	return nil
}

Handlers.go

func (app *appInjection) RegisterUser(w http.ResponseWriter, r *http.Request) {

	// Guide addressing headers, syntax error's, and preventing extra data fields
	// https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body
	w.Header().Set("Content-Type", "application/json")
	var newUser models.User
	//Parse the form data
	//err := json.NewDecoder(r.Body).Decode(&newUser)
	err := app.decodeJSONBody(w, r, &newUser)
	if err != nil {
		var mr *malformedRequest
		if errors.As(err, &mr) {
			http.Error(w, mr.msg, mr.status)
			//app.clientError(w, mr.status)
		} else {
			log.Println(err.Error())
			//app.clientError(w, http.StatusInternalServerError)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}
		return
	}

	//TODO: Validate the form
	//If there is no error and the form is validated, create a new user from http request
	//Insert the new user into the database
	uid, _ := app.user.Insert(
		newUser.FirstName,
		newUser.LastName,
		newUser.Email,
		newUser.Phone,
		string(newUser.Password))

	json.NewEncoder(w).Encode("Record Inserted")
	json.NewEncoder(w).Encode(uid)
}

User.go

func (u *UserFunctions) Insert(firstname, lastname, email, phone, password string) (primitive.ObjectID, error) {
	//Insert user to the database
	userCollection := u.CLIENT.Database("queue").Collection("users")
	var user models.User

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		object, _ := primitive.ObjectIDFromHex("")
		return object, err
	}
	user.FirstName = firstname
	user.LastName = lastname
	user.Email = email
	user.Phone = phone
	user.Password = hashedPassword
	user.Created = time.Now().UTC()
	user.Active = true

	//Insert the user into the database
	result, err := userCollection.InsertOne(context.TODO(), user)
	if err != nil {
		fmt.Println(err)
	}

	//Check ID of the inserted document
	insertedID := result.InsertedID.(primitive.ObjectID)
	//fmt.Println(insertedID)

	return insertedID, nil
}

当我在Postman中运行注册用户的请求时,它会抛出以下消息:

Request body contains unknown field "password"
英文:

I'm not sure what is causing this, but the reason it throws it is when I call dec.DisallowUnknownFields() it somehow thinks that password isn't a field in the User struct/request body. Albeit, the password JSON is "-". So I thought originally to change the JSON to "password" for struct field Password but that throws an

internal server error:  illegal base64 data at input byte 4

Request Body in Postman

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "george@gmail.com",
    "phone": "703-123-4567",
    "password": "abc12"
}

I also tried to change the type of password to string, which also didn't work but arguably would be the wrong solution because we should store passwords as hashes in the DB. So, at this point I've run out of places to turn to as to why this is happening...

models.go

type User struct {
	ID primitive.ObjectID    `json:"_id,omitempty" bson:"_id,omitempty"`
	FirstName      string    `json:"firstname"`
	LastName       string    `json:"lastname"`
	Email          string    `json:"email"`
	Phone 		   string    `json:"phone"`
	Password 	   []byte    `json:"-"`
	Created        time.Time `json:"-"`
	Active         bool      `json:"-"`
	Address        Address   `json:"address,omitempty"`
}

Helpers.go

type malformedRequest struct {
	status int
	msg    string
}

func (mr *malformedRequest) Error() string {
	return mr.msg
}

func (app *appInjection) decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error {
	if r.Header.Get("Content-Type") != "" {
		value, _ := header.ParseValueAndParams(r.Header, "Content-Type")
		if value != "application/json" {
			msg := "Content-Type header is not application/json"
			return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: msg}
		}
	}

	r.Body = http.MaxBytesReader(w, r.Body, 1048576)

	dec := json.NewDecoder(r.Body)
	dec.DisallowUnknownFields()
	err := dec.Decode(&dst)

	if err != nil {
		var syntaxError *json.SyntaxError
		var unmarshalTypeError *json.UnmarshalTypeError

		switch {
		case errors.As(err, &syntaxError):
			msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.Is(err, io.ErrUnexpectedEOF):
			msg := fmt.Sprintf("Request body contains badly-formed JSON")
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.As(err, &unmarshalTypeError):
			msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case strings.HasPrefix(err.Error(), "json: unknown field "):
			fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
			msg := fmt.Sprintf("Request body contains unknown field %s", fieldName)
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case errors.Is(err, io.EOF):
			msg := "Request body must not be empty"
			return &malformedRequest{status: http.StatusBadRequest, msg: msg}

		case err.Error() == "http: request body too large":
			msg := "Request body must not be larger than 1MB"
			return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: msg}

		default:
			return err
		}
	}

	err = dec.Decode(&struct{}{})
	if err != io.EOF {
		msg := "Request body must only contain a single JSON object"
		return &malformedRequest{status: http.StatusBadRequest, msg: msg}
	}

	return nil
}

Handlers.go

func (app *appInjection) RegisterUser(w http.ResponseWriter, r *http.Request) {

	// Guide addressing headers, syntax error's, and preventing extra data fields
	// https://www.alexedwards.net/blog/how-to-properly-parse-a-json-request-body
	w.Header().Set("Content-Type", "application/json")
	var newUser models.User
	//Parse the form data
	//err := json.NewDecoder(r.Body).Decode(&newUser)
	err := app.decodeJSONBody(w, r, &newUser)
	if err != nil {
		var mr *malformedRequest
		if errors.As(err, &mr) {
			http.Error(w, mr.msg, mr.status)
			//app.clientError(w, mr.status)
		} else {
			log.Println(err.Error())
			//app.clientError(w, http.StatusInternalServerError)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		}
		return
	}

	//TODO: Validate the form
	//If there is no error and the form is validated, create a new user from http request
	//Insert the new user into the database
	uid, _ := app.user.Insert(
		newUser.FirstName,
		newUser.LastName,
		newUser.Email,
		newUser.Phone,
		string(newUser.Password))

	json.NewEncoder(w).Encode("Record Inserted")
	json.NewEncoder(w).Encode(uid)
}

User.go

func (u *UserFunctions) Insert(firstname, lastname, email, phone, password string) (primitive.ObjectID, error) {
	//Insert user to the database
	userCollection := u.CLIENT.Database("queue").Collection("users")
	var user models.User

	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
	if err != nil {
		object, _ := primitive.ObjectIDFromHex("")
		return object, err
	}
	user.FirstName = firstname
	user.LastName = lastname
	user.Email = email
	user.Phone = phone
	user.Password = hashedPassword
	user.Created = time.Now().UTC()
	user.Active = true

	//Insert the user into the database
	result, err := userCollection.InsertOne(context.TODO(), user)
	if err != nil {
		fmt.Println(err)
	}

	//Check ID of the inserted document
	insertedID := result.InsertedID.(primitive.ObjectID)
	//fmt.Println(insertedID)

	return insertedID, nil
}

When I run the request to sign up a user in Postman it throws the message

Request body contains unknown field "password"

答案1

得分: 1

正如其他人已经广泛提到的那样,由于JSON不能直接包含字节,Go的JSON解析器期望任何[]byte对象在输入数据中以Base64形式存储。

因此,你的问题不一定在于Go端。你的问题是你告诉Go期望的是[]byte,即Base64编码,但你告诉客户端发送的是“abc12”,这不是一个有效的Base64值。

对于你所描述的情况,一个简单的解决方案是发送相同字符串的Base64编码版本:

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "george@gmail.com",
    "phone": "703-123-4567",
    "password": "YWJjMTIK"
}

但我想深入一点。

string [...] 可能是错误的解决方案,因为我们应该将密码存储为哈希值在数据库中

你可以将密码以字节的形式哈希存储在数据库中,但这不是从curl中传入的数据。所以对我来说,你真正的问题是你将两种不同类型的数据——用户提供的密码和数据库中的哈希表示——尝试混合到同一个数据空间中,这完全没有意义。毕竟,如果你要设置密码,当前的哈希(如果有)是无关紧要的,如果你要检查密码,你应该检查用户输入的内容。

这是我更喜欢的解决方案:

type User struct {
    ... ... ...
    HashedPassword []byte `json:"-"`
    Password       string `json:"password"`
}
英文:

As folks already linked extensively, since JSON cannot contain bytes directly, the Go Json parser expects any []byte objects to be stored in Base64 in the input data.

Your problem, therefore, is not necessarily on the Go end. Your problem is a mismatch with what you told Go to expect - []byte, Base64 encoded - and what you told the client to send - "abc12" - which is not a valid base64 value.

One simple solution for your stated case would be to send a base64 encoded version of the same string:

{
    "firstname": "George",
    "lastname": "Costanza",
    "email": "george@gmail.com",
    "phone": "703-123-4567",
    "password": "YWJjMTIK"
}

But I'd like to dig just a little bit deeper.

> string [...] arguably would be the wrong solution because we should store passwords as hashes in the DB

You might store passwords hashed into bytes in the database, but that's not what's coming int from curl . So to me, your real problem is that you took 2 different kinds of data - user provided password and DB hashed representation - and tried to mash them into the same data space, which has absolutely no value. After all, if you were setting a password, the current hash, if any, is irrelevant, and if you're checking the password, you'd be checking what they inputted.

This is the solution I'd prefer:

type User struct {
    ... ... ...
    HashedPassword       []byte    `json:"-"`
    Password string `json:"password"`

}

答案2

得分: 0

一个简单的解决方案是:

var newUser struct {
    models.User
    Password string `json:"password"`
}
// 解析表单数据
// err := json.NewDecoder(r.Body).Decode(&newUser)
err := app.decodeJSONBody(w, r, &newUser)

这段代码定义了一个名为newUser的结构体变量,其中包含了models.User的字段和一个名为Password的字段。Password字段使用json:"password"的标签指定了在JSON中的键名为password

接下来的代码通过调用app.decodeJSONBody函数来解析请求的JSON数据,并将解析结果存储到newUser变量中。

英文:

A simple solution would be

var newUser struct {
    models.User
    Password string `json:"password"`
}
//Parse the form data
//err := json.NewDecoder(r.Body).Decode(&newUser)
err := app.decodeJSONBody(w, r, &newUser)

huangapple
  • 本文由 发表于 2021年10月9日 00:45:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/69499232.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定