更优雅的验证go-gin中的请求体的方法

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

More elegant way of validate body in go-gin

问题

有没有更优雅的方法来使用go-gin验证json bodyroute id?以下是代码的翻译:

package controllers

import (
	"giin/inputs"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

func GetAccount(context *gin.Context) {

	// 验证`accountId`是否为有效的`uuid`
	_, err := uuid.Parse(context.Param("accountId"))
	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}

	// 这里是一些逻辑...

	context.JSON(http.StatusOK, gin.H{"message": "收到账户"})
}

func AddAccount(context *gin.Context) {

	// 验证`body`是否为有效的`inputs.Account`
	var input inputs.Account
	if error := context.ShouldBindJSON(&input); error != nil {
		context.JSON(http.StatusBadRequest, error.Error())
		return
	}

	// 这里是一些逻辑...

	context.JSON(http.StatusOK, gin.H{"message": "添加账户"})
}

我创建了一个中间件,能够检测是否传递了accountId,如果是,则验证它并返回错误请求,如果accountId不是uuid格式。但是对于body,我无法做到同样的事情,因为AccountBodyMiddleware尝试验证每个请求,有人能帮我解决这个问题吗?

而且,如果我能够验证任何类型的body,而不是为每个json body创建新的中间件,那将非常好。

package main

import (
	"giin/controllers"
	"giin/inputs"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

func AccountIdMiddleware(c *gin.Context) {
	id := c.Param("accountId")
	if id == "" {
		c.Next()
		return
	}
	if _, err := uuid.Parse(id); err != nil {
		c.JSON(http.StatusBadRequest, "uuid无效")
		c.Abort()
		return
	}
}

func AccountBodyMiddleware(c *gin.Context) {
	var input inputs.Account
	if error := c.ShouldBindJSON(&input); error != nil {
		c.JSON(http.StatusBadRequest, "body无效")
		c.Abort()
		return
	}
	c.Next()
}

func main() {
	r := gin.Default()
	r.Use(AccountIdMiddleware)
	r.Use(AccountBodyMiddleware)

	r.GET("/account/:accountId", controllers.GetAccount)
	r.POST("/account", controllers.AddAccount)
	r.Run(":5000")
}

希望对你有所帮助!

英文:

Is there a more elegant way to validate json body and route id using go-gin?

package controllers

import (
	"giin/inputs"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

func GetAccount(context *gin.Context) {

	// validate if `accountId` is valid `uuid``
	_, err := uuid.Parse(context.Param("accountId"))
	if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
		return
	}

	// some logic here...

	context.JSON(http.StatusOK, gin.H{"message": "account received"})
}

func AddAccount(context *gin.Context) {

	// validate if `body` is valid `inputs.Account`
	var input inputs.Account
	if error := context.ShouldBindJSON(&input); error != nil {
		context.JSON(http.StatusBadRequest, error.Error())
		return
	}

	// some logic here...

	context.JSON(http.StatusOK, gin.H{"message": "account added"})
}

I created middleware which is able to detect if accountId was passed and if yes validate it and return bad request if accountId was not in uuid format but I couldn't do the same with the body because AccountBodyMiddleware tries to validate every request, could someone help me with this?

And also it would be nice if I could validate any type of body instead creating new middleware for each json body

package main

import (
	"giin/controllers"
	"giin/inputs"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
)

func AccountIdMiddleware(c *gin.Context) {
	id := c.Param("accountId")
	if id == "" {
		c.Next()
		return
	}
	if _, err := uuid.Parse(id); err != nil {
		c.JSON(http.StatusBadRequest, "uuid not valid")
		c.Abort()
		return
	}
}

func AccountBodyMiddleware(c *gin.Context) {
	var input inputs.Account
	if error := c.ShouldBindJSON(&input); error != nil {
		c.JSON(http.StatusBadRequest, "body is not valid")
		c.Abort()
		return
	}
	c.Next()
}

func main() {
	r := gin.Default()
	r.Use(AccountIdMiddleware)
	r.Use(AccountBodyMiddleware)

	r.GET("/account/:accountId", controllers.GetAccount)
	r.POST("/account", controllers.AddAccount)
	r.Run(":5000")
}

答案1

得分: 2

使用中间件在这里肯定不是正确的方法,你的直觉是正确的!受到FastAPI的启发,我通常为每个请求/响应创建模型。然后,你可以将这些模型绑定为查询、路径或正文模型。这是一个查询模型绑定的示例(只是为了向你展示你可以将其用于更多类型的请求,而不仅仅是JSON POST请求):

type User struct {
	UserId            string                     `form:"user_id"`
	Name              string                     `form:"name"`
}

func (user *User) Validate() errors.RestError {
    if _, err := uuid.Parse(id); err != nil {
        return errors.BadRequestError("user_id不是有效的UUID")
    }
    return nil
}

其中,errors只是一个你可以在本地定义的包,以便可以直接返回验证错误,如下所示:

func GetUser(c *gin.Context) {

	// 绑定查询模型
    var q User
	if err := c.ShouldBindQuery(&q); err != nil {
		restError := errors.BadRequestError(err.Error())
		c.JSON(restError.Status, restError)
		return
	}

	// 验证请求
	if err := q.Validate(); err != nil {
		c.JSON(err.Status, err)
		return
	}

    // 业务逻辑在这里
}

额外奖励:通过这种方式,你还可以组合结构体并从高层调用内部验证函数。我认为这就是你试图通过使用中间件来实现的(组合验证):

type UserId struct {
    Id string
}

func (userid *UserId) Validate() errors.RestError {
    if _, err := uuid.Parse(id); err != nil {
        return errors.BadRequestError("user_id不是有效的UUID")
    }
    return nil
}

type User struct {
    UserId
    Name string 
}

func (user *User) Validate() errors.RestError {
    if err := user.UserId.Validate(); err != nil {
        return err
    }

    // 进行其他验证
    
    return nil
}

额外奖励:如果你有兴趣,可以在这里阅读更多关于后端路由设计和基于模型的验证的内容Softgrade - 后端路由设计深度指南

供参考,这是一个错误结构的示例:

type RestError struct {
	Message string `json:"message"`
	Status  int    `json:"status"`
	Error   string `json:"error"`
}

func BadRequestError(message string) *RestError {
	return &RestError{
		Message: message,
		Status:  http.StatusBadRequest,
		Error:   "无效的请求",
	}
}
英文:

Using middlewares is certainly not the way to go here, your hunch is correct! Using FastAPI as inspiration, I usually create models for every request/response that I have. You can then bind these models as query, path, or body models. An example of query model binding (just to show you that you can use this to more than just json post requests):

type User struct {
	UserId            string                     `form:"user_id"`
	Name              string                     `form:"name"`
}

func (user *User) Validate() errors.RestError {
    if _, err := uuid.Parse(id); err != nil {
        return errors.BadRequestError("user_id not a valid uuid")
    }
    return nil
}

Where errors is just a package you can define locally, so that can return validation errors directly in the following way:

func GetUser(c *gin.Context) {

	// Bind query model
    var q User
	if err := c.ShouldBindQuery(&q); err != nil {
		restError := errors.BadRequestError(err.Error())
		c.JSON(restError.Status, restError)
		return
	}

	// Validate request
	if err := q.Validate(); err != nil {
		c.JSON(err.Status, err)
		return
	}

    // Business logic goes here
}

Bonus: In this way, you can also compose structs and call internal validation functions from a high level. I think this is what you were trying to accomplish by using middlewares (composing validation):

type UserId struct {
    Id string
}

func (userid *UserId) Validate() errors.RestError {
    if _, err := uuid.Parse(id); err != nil {
        return errors.BadRequestError("user_id not a valid uuid")
    }
    return nil
}

type User struct {
    UserId
    Name string 
}

func (user *User) Validate() errors.RestError {
    if err := user.UserId.Validate(); err != nil {
        return err
    }

    // Do some other validation
    
    return nil
}

Extra bonus: read more about backend route design and model-based validation here if you're interested Softgrade - In Depth Guide to Backend Route Design

For reference, here is an example errors struct:

type RestError struct {
	Message string `json:"message"`
	Status  int    `json:"status"`
	Error   string `json:"error"`
}

func BadRequestError(message string) *RestError {
	return &RestError{
		Message: message,
		Status:  http.StatusBadRequest,
		Error:   "Invalid Request",
	}
}

huangapple
  • 本文由 发表于 2022年1月25日 20:40:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/70848804.html
匿名

发表评论

匿名网友

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

确定