• validator库的使用详解


    基本使用

    前言

    在做API开发时,需要对请求参数的校验,防止用户的恶意请求。例如日期格式,用户年龄,性别等必须是正常的值,不能随意设置。以前会使用大量的if判断参数的值是否符合规范,现在可以使用validator库来进行参数校验。我们只需要在结构体的Tag中添加 validator 标签就可以实现参数校验。同时Gin框架当前内部也集成了validator.v10这个库,在Gin框架中只要在结构体的Tag中添加 binding 标签就可以实现参数校验。使用binding标签和validator标签都是可以的,但是在Gin框架中我们一般会使用binding标签来实现参数校验。

    请求模型的定义

    //请求的参数必须存在并且由数字或字母组成
    type UserRequest struct {
    	Username string `json:"username" form:"username" binding:"required,alphanum"`
    	Password string `json:"password" form:"password" binding:"required,alphanum"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编写接口及测试

    // ModifyUser 用来修改用户的密码
    func ModifyUser(c *gin.Context) {
    	var user UserRequest
    	if err := c.BindQuery(&user); err != nil {
    		c.JSON(http.StatusOK, gin.H{
    			"msg": err.Error(),
    		})
    		return
    	}
    	//查询是否存在该用户
    	result := service.SelectUserIfExist(user.Username)
    	if result == 1 { //该用户存在
    		userInfo := service.ChangeUserSecret(user.Username, user.Password)
    		c.JSON(http.StatusOK, gin.H{
    			"msg": userInfo,
    		})
    	} else {
    		c.JSON(http.StatusOK, gin.H{
    			"msg": "用户不存在",
    		})
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    现在数据库中有一个用户。

    mysql> select * from user;
    +---------+----------+----------+
    | user_id | username | password |
    +---------+----------+----------+
    |       4 | abc123   | abc123   |
    +---------+----------+----------+
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们在修改密码时,将密码设置为空或者改成 xyz@456 看看参数校验能不能通过。

    image-20231011173623783 image-20231011173823585

    可以看到将密码设置为空或者改成 xyz@456 会曝出不同的错误。下面使用满足校验规则的密码进行测试:

    image-20231011174027471

    可以看到密码已经被成功修改。

    翻译校验错误提示信息

    当参数校验错误的时候会返回错误信息,可以看到上面的错误信息都是英文,有时候看英文确实会不太舒服,所以也没有什么办法可以把错误信息翻译成中文?答案是有的,validator 库本身是支持国际化的,借助相应的语言包可以实现校验错误提示信息的自动翻译。下面的示例代码演示了如何将错误提示信息翻译成中文。可以新建一个目录utils,目录下面新建一个translate.go文件。文件内容如下:

    package utils
    
    import (
    	"fmt"
    	"github.com/gin-gonic/gin/binding"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	enTranslations "github.com/go-playground/validator/v10/translations/en"
    	zhTranslations "github.com/go-playground/validator/v10/translations/zh"
    )
    
    // Trans 一个全局翻译器
    var Trans ut.Translator
    
    func Translate(locale string) (err error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if value, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
    		zhT := zh.New() // 中文翻译器
    		enT := en.New() // 英文翻译器
    
    		// 第一个参数是备用(fallback)的语言环境
    		// 后面的参数是支持的语言环境(支持多个)
    		uni := ut.New(enT, zhT, enT)
    
    		// locale 通常取决于 http 请求头的 'Accept-Language'
    		var ok bool
    		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
    		Trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
    		}
    
    		// 注册翻译器
    		switch locale {
    		case "en":
    			err = enTranslations.RegisterDefaultTranslations(value, Trans)
    		case "zh":
    			err = zhTranslations.RegisterDefaultTranslations(value, Trans)
    		default:
    			err = enTranslations.RegisterDefaultTranslations(value, Trans)
    		}
    		return
    	}
    	return
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    然后需要在main函数中初始化 Translate 这个函数。

    func main() {
    	if err := utils.Translate("zh"); err != nil {
    		fmt.Printf("init trans failed, err:%v\n", err)
    		return
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接下来就能使用这个翻译器翻译英文信息。

    // ModifyUser 用来修改用户的密码
    func ModifyUser(c *gin.Context) {
    	var user UserRequest
    	if err := c.ShouldBindQuery(&user); err != nil {
    		// 获取validator.ValidationErrors类型的errors
    		errs, ok := err.(validator.ValidationErrors)
    		if !ok {
    			// 非validator.ValidationErrors类型错误直接返回
    			c.JSON(http.StatusOK, gin.H{
    				"msg": err.Error(),
    			})
    			return
    		}
    		// validator.ValidationErrors类型错误则进行翻译
    		c.JSON(http.StatusOK, gin.H{
    			"msg": errs.Translate(utils.Trans),
    		})
    		return
    	}
    	//查询是否存在该用户
    	result := service.SelectUserIfExist(user.Username)
    	if result == 1 { //该用户存在
    		userInfo := service.ChangeUserSecret(user.Username, user.Password)
    		c.JSON(http.StatusOK, gin.H{
    			"msg": userInfo,
    		})
    	} else {
    		c.JSON(http.StatusOK, gin.H{
    			"msg": "用户不存在",
    		})
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    测试结果如下:

    image-20231011192954175 image-20231011192918100

    自定义错误提示信息的字段名

    上面的错误提示看起来是可以了,但是还是差点意思,首先是错误提示中的字段并不是请求中使用的字段,例如:Password 是我们后端定义的结构体中的字段名,而请求中使用的是小写的 password 字段。如何让错误提示中的字段使用自定义的名称,例如json tag指定的值呢?

    其实只需要在初始化翻译器的时候像下面一样添加一个获取json tag的自定义方法即可。

    func Translate(locale string) (err error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if value, ok := binding.Validator.Engine().(*validator.Validate); ok {
    	
    		// 注册一个获取json tag的自定义方法
    		value.RegisterTagNameFunc(func(fld reflect.StructField) string {
    			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    			if name == "-" {
    				return ""
    			}
    			return name
    		})
    	
            zhT := zh.New() // 中文翻译器
            enT := en.New() // 英文翻译器
    
            // 第一个参数是备用(fallback)的语言环境
            // 后面的参数是支持的语言环境(支持多个)
            uni := ut.New(enT, zhT, enT)
    
            ······
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    现在错误信息中的password就变成了小写了。测试结果如下:

    image-20231011194814448

    但是还是有点瑕疵,那就是最终的错误提示信息中心还是有我们后端定义的结构体名称(UserRequest),这个名称其实是不需要随错误提示返回给前端的,前端并不需要这个值。我们需要想办法把它去掉。其实可以定义一个去掉结构体名称前缀的自定义方法:

    func removeTopStruct(fields map[string]string) map[string]string {
    	res := map[string]string{}
    	for field, err := range fields {
    		res[field[strings.Index(field, ".")+1:]] = err
    	}
    	return res
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们在代码中使用上述函数将翻译后的 errors 做一下处理即可:

    if err := c.ShouldBindQuery(&user); err != nil {
    		// 获取validator.ValidationErrors类型的errors
    		errs, ok := err.(validator.ValidationErrors)
    		if !ok {
    			// 非validator.ValidationErrors类型错误直接返回
    			c.JSON(http.StatusOK, gin.H{
    				"msg": err.Error(),
    			})
    			return
    		}
    		// validator.ValidationErrors类型错误则进行翻译
        	// 并使用removeTopStruct函数去除字段名中的结构体名称标识
    		c.JSON(http.StatusOK, gin.H{
    			"msg": removeTopStruct(errs.Translate(utils.Trans)),
    		})
    		return
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    测试结果如下:

    image-20231011195855386

    自定义校验规则

    先在又有一个需求,那就是要求密码必须是由数字、字母或下划线组成,且第一个字符不能是下划线,那么这个需求又该如何实现呢?

    func CustomFunc(fl validator.FieldLevel) bool {
    	// 正则表达式来匹配字母、数字和下划线,且第一个字符不能是下划线
    	re := regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_]*$`)
    	return re.MatchString(fl.Field().String())
    }
    
    // tag参数就是我们自己自定义的校验规则的名字
    func Custom(tag string) (err error) {
    	if value, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		// 在校验器注册自定义的校验方法
    		if err := value.RegisterValidation(tag, CustomFunc); err != nil {
    			return err
    		}
    	}
    	return err
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    接下来只需要在main函数或者一些初始化函数当中注册这个Custom函数就可以了。需要注意的是,要添加上binding的字段校验规则,比如:

    // Password的校验规则就是我们自定义的校验规则:password
    type UserRequest struct {
    	Username string `json:"username" form:"username" binding:"required,alphanum"`
    	Password string `json:"password" form:"password" binding:"required,password"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    测试结果如下:

    image-20231011204702960 image-20231011204618021

    可以看到 abc_123 满足校验规则,而 _abc123 不满足校验规则。

    常见的参数校验字段

    下面是一些常见的校验规则:

    Tag描述
    eqfield一个字段等于另一个字段
    alpha仅限字母
    alphanum仅限字母数字
    excludes排除
    jsonJSON
    jwtJSON Web Token (JWT)
    emailE-mail 字符串
    html_encodedHTML编码
    eq等于
    gt大于
    gte大于或等于
    lt小于
    lte小于或等于
    ne不等于
    len长度
    max最大
    min最小
    required必需的
    unique唯一
  • 相关阅读:
    风控ML[16] | 风控建模中怎么做拒绝推断
    【MyBatis】动态SQL
    LabVIEW中不同颜色连线的含义
    python图形界面化编程GUI(五)坦克大战(一)
    船舶稳定性和静水力计算——绘图体平面图,静水力,GZ计算(Matlab代码实现)
    Spring系统学习 -Spring IOC 的XML管理Bean之类类型属性赋值、数组类型属性赋值、集合类属性赋值
    TypeScript基础语法
    带你读论文丨S&P21 Survivalism: Living-Off-The-Land 经典离地攻击
    位于kernel的文件系统大管家--Virtual File System
    【Unity基础】1.项目搭建与视图编辑
  • 原文地址:https://blog.csdn.net/qq_54015483/article/details/133778967