G

[Golang]Gin框架 6.验证器/校验器

RoLingG 2023-10-04

验证器

1.常用的验证器

//不能为空,并且不能没有这个字段
required: 必填字段,如:binding:"required"

//针对字符串的长度 
min 最小长度,如:binding:"min=5" 
max 最大长度,如:binding:"max=10" 
len 长度,如:binding:"len=6" 

//针对数字的大小
eq 等于,如:binding:"eq=3" 
ne 不等于,如:binding:"ne=12" 
gt 大于,如:binding:"gt=10" 
gte 大于等于,如:binding:"gte=10" 
lt 小于,如:binding:"lt=10" 
lte 小于等于,如:binding:"lte=10" 

// 针对同级字段的
eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"`
nefield 不等于其他字段的值  
//↑用于确认密码等,注意,binding:"eqfield=sth"的sth是参数名,而非绑定的Json名

//特殊
- 忽略字段,如:binding:"-"

如果绑定的参数是数组,那么这些传统的验证器便没有办法让数组内每一个参数都进行验证,这是这些常用数组的弊端。
所以就要用如下Gin内置的验证器进行完善,例如针对邮箱、ip地址等。

——————————————————————————————————————————————————

2.Gin内置的验证器

//枚举 只能是列举的情况
oneof=red green    //←只能是red或者是green

// 字符串  
contains=name  // 包含name的字符串
excludes // 不包含
startswith  // 字符串前缀
endswith  // 字符串后缀

// 数组
dive  // dive后面的验证就是针对数组中的每一个元素

// 网络验证
ip
ipv4
ipv6
uri
url
// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。
// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径

Apipost的例子如下:
{
    "name": "RoLingG",
    "age": 23,
    "sex": "male",
    "likeList": [
        "Like1", 
        "Like2", 
        "Like3"
    ],
    "ip":"192.168.1.1",
    "url":"https://rolingg.top/root/image",
//  "uri":"https://rolingg.top/root/image"    这个也是可以的
    "uri":"/root/image"
}

// 日期验证  1月2号下午3点4分5秒在2006年
datetime=2006-01-02 03:04:05    //这个时间很特殊
格式含义
01、 1、Jan、January
02、 2、_2日,这个_2表示如果日期是只有一个数字,则表示出来的日期前面用个空格占位。
03、 3、15
04、4
05、5
2006、06、6
-070000、 -07:00:00、 -0700、 -07:00、 -07Z070000、Z07:00:00、 Z0700、 Z07:00时区
PM、pm上、下午
Mon、Monday星期
MST美国时间,如果机器设置的是中国时间则表示为UTC

补充一下2006-01-02 03:04:05之所以特殊的原因:如上表格你们也可以看到这串时间的数字基本符合上表格所对应的内容,这个时间之所以特殊是因为Go语言源码判断时间需要通过特殊的数字进行开始判断,Go语言开发者为了解决时间这个问题就用了这种很鸡贼的方法。

↓下面是Go语言原生源码,需要了解的可以尝试看懂下面的源码内容↓。

func nextStdChunk(layout string) (prefix string, std int, suffix string) {
    for i := 0; i < len(layout); i++ {
        switch c := int(layout[i]); c {
        case 'J': // January, Jan
            if len(layout) >= i+3 && layout[i:i+3] == "Jan" {
                if len(layout) >= i+7 && layout[i:i+7] == "January" {
                    return layout[0:i], stdLongMonth, layout[i+7:]
                }
                if !startsWithLowerCase(layout[i+3:]) {
                    return layout[0:i], stdMonth, layout[i+3:]
                }
            }

        case 'M': // Monday, Mon, MST
            if len(layout) >= i+3 {
                if layout[i:i+3] == "Mon" {
                    if len(layout) >= i+6 && layout[i:i+6] == "Monday" {
                        return layout[0:i], stdLongWeekDay, layout[i+6:]
                    }
                    if !startsWithLowerCase(layout[i+3:]) {
                        return layout[0:i], stdWeekDay, layout[i+3:]
                    }
                }
                if layout[i:i+3] == "MST" {
                    return layout[0:i], stdTZ, layout[i+3:]
                }
            }

        case '0': // 01, 02, 03, 04, 05, 06
            if len(layout) >= i+2 && '1' <= layout[i+1] && layout[i+1] <= '6' {
                return layout[0:i], std0x[layout[i+1]-'1'], layout[i+2:]
            }

        case '1': // 15, 1
            if len(layout) >= i+2 && layout[i+1] == '5' {
                return layout[0:i], stdHour, layout[i+2:]
            }
            return layout[0:i], stdNumMonth, layout[i+1:]

        case '2': // 2006, 2
            if len(layout) >= i+4 && layout[i:i+4] == "2006" {
                return layout[0:i], stdLongYear, layout[i+4:]
            }
            return layout[0:i], stdDay, layout[i+1:]

        case '_': // _2, _2006
            if len(layout) >= i+2 && layout[i+1] == '2' {
                //_2006 is really a literal _, followed by stdLongYear
                if len(layout) >= i+5 && layout[i+1:i+5] == "2006" {
                    return layout[0 : i+1], stdLongYear, layout[i+5:]
                }
                return layout[0:i], stdUnderDay, layout[i+2:]
            }

        case '3':
            return layout[0:i], stdHour12, layout[i+1:]

        case '4':
            return layout[0:i], stdMinute, layout[i+1:]

        case '5':
            return layout[0:i], stdSecond, layout[i+1:]

        case 'P': // PM
            if len(layout) >= i+2 && layout[i+1] == 'M' {
                return layout[0:i], stdPM, layout[i+2:]
            }

        case 'p': // pm
            if len(layout) >= i+2 && layout[i+1] == 'm' {
                return layout[0:i], stdpm, layout[i+2:]
            }

        case '-': // -070000, -07:00:00, -0700, -07:00, -07
            if len(layout) >= i+7 && layout[i:i+7] == "-070000" {
                return layout[0:i], stdNumSecondsTz, layout[i+7:]
            }
            if len(layout) >= i+9 && layout[i:i+9] == "-07:00:00" {
                return layout[0:i], stdNumColonSecondsTZ, layout[i+9:]
            }
            if len(layout) >= i+5 && layout[i:i+5] == "-0700" {
                return layout[0:i], stdNumTZ, layout[i+5:]
            }
            if len(layout) >= i+6 && layout[i:i+6] == "-07:00" {
                return layout[0:i], stdNumColonTZ, layout[i+6:]
            }
            if len(layout) >= i+3 && layout[i:i+3] == "-07" {
                return layout[0:i], stdNumShortTZ, layout[i+3:]
            }

        case 'Z': // Z070000, Z07:00:00, Z0700, Z07:00,
            if len(layout) >= i+7 && layout[i:i+7] == "Z070000" {
                return layout[0:i], stdISO8601SecondsTZ, layout[i+7:]
            }
            if len(layout) >= i+9 && layout[i:i+9] == "Z07:00:00" {
                return layout[0:i], stdISO8601ColonSecondsTZ, layout[i+9:]
            }
            if len(layout) >= i+5 && layout[i:i+5] == "Z0700" {
                return layout[0:i], stdISO8601TZ, layout[i+5:]
            }
            if len(layout) >= i+6 && layout[i:i+6] == "Z07:00" {
                return layout[0:i], stdISO8601ColonTZ, layout[i+6:]
            }
            if len(layout) >= i+3 && layout[i:i+3] == "Z07" {
                return layout[0:i], stdISO8601ShortTZ, layout[i+3:]
            }

        case '.': // .000 or .999 - repeated digits for fractional seconds.
            if i+1 < len(layout) && (layout[i+1] == '0' || layout[i+1] == '9') {
                ch := layout[i+1]
                j := i + 1
                for j < len(layout) && layout[j] == ch {
                    j++
                }
                // String of digits must end here - only fractional second is all digits.
                if !isDigit(layout, j) {
                    std := stdFracSecond0
                    if layout[i+1] == '9' {
                        std = stdFracSecond9
                    }
                    std |= (j - (i + 1)) << stdArgShift
                    return layout[0:i], std, layout[j:]
                }
            }
        }
    }
    return layout, 0, ""
}

——————————————————————————————————————————————————

3.自定义验证错误信息

当验证不通过时,会给出错误信息,但原始信息的错误不友好,不利于用户观看。

只需要给结构体加一个msg的tag即可。

例如:

//↓需要的JSON
{
    "name": "RoLingG",
       "age": 23,
       "sex": "male",
       "likeList": [
       "Like1",
       "Like2",
       "Like3"
    ],
       "ip":"192.168.1.1",
       "url":"https://rolingg.top/root/image",
       "uri":"/root/image",
       "date":"2023-01-01 15:04:06"
}

//↓现在的JSON
{
    "age": 18
}

//错误信息如下:
{
    "msg": "Key: 'SignUserInfo.Name' Error:Field validation for 'Name' failed on the 'required' tag\nKey: 'SignUserInfo.Sex' Error:Field validation for 'Sex' failed on the 'oneof' tag\nKey: 'SignUserInfo.LikeList' Error:Field validation for 'LikeList' failed on the 'required' tag\nKey: 'SignUserInfo.IP' Error:Field validation for 'IP' failed on the 'required' tag\nKey: 'SignUserInfo.Url' Error:Field validation for 'Url' failed on the 'url' tag\nKey: 'SignUserInfo.Uri' Error:Field validation for 'Uri' failed on the 'uri' tag\nKey: 'SignUserInfo.Date' Error:Field validation for 'Date' failed on the 'datetime' tag"
}
//↑现在就可以很直观的看出来默认未经过设置的错误信息过于冗余且不适合人去观看,所以自定义错误信息还是很有必要的

所以自定义错误信息对于日后查询问题或者维护很有必要

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
    "reflect"
)

// 封装化 返回结构体中的msg参数
//
//    这样写的好处是错误信息容易看得懂,易获取,但相对的他只能从上到下的优先级顺序去返回错误信息,一次一条(应该能自改成多条),如果要显示多条的话工作量会上升。
func GetValidMsg(err error, obj any) string {
    //obj不用指针的原因是因为传过来user的时候已经是指针的形式了,所以不用再转一遍成指针
    getobj := reflect.TypeOf(obj)
    //把error接口断言为具体类型
    if errs, ok := err.(validator.ValidationErrors); ok {
        //断言成功
        //errs是一个切片
        for _, e := range errs {
            //循环每一个错误信息
            //接下来要用结构体的方式去反射msg的类型 ①
            //去拿标签的原始值,根据报错字段名,获取结构体的具体字段    ③
            if f, exist := getobj.Elem().FieldByName(e.Field()); exist {
                //可以用这种,也可以用map的方式,但是map的方式要尽可能有获取的焦点,不然也没啥用
                msg := f.Tag.Get("msg")
                return msg
            }
        }
    }
    return err.Error()
}

func main() {
    router := gin.Default()
    router.POST("/", func(c *gin.Context) {
        type User struct {
            Name string `json:"name" binding:"required" msg:"用户名校验失败"`
            Age  int    `json:"age" binding:"required" msg:"年龄校验失败"`
        }
        var user User
        err := c.ShouldBindJSON(&user)
        if err != nil {
            //未封装化
            //拿msg的类型,要结构体的指针②
            //getobj := reflect.TypeOf(&user)
            ////把error接口断言为具体类型
            //if errs, ok := err.(validator.ValidationErrors); ok {
            //    //断言成功
            //    //errs是一个切片
            //    for _, e := range errs {
            //        //循环每一个错误信息
            //        //接下来要用结构体的方式去反射msg的类型 ①
            //        //去拿标签的原始值,根据报错字段名,获取结构体的具体字段    ③
            //        if f, exist := getobj.Elem().FieldByName(e.Field()); exist {
            //            //可以用这种,也可以用map的方式,但是map的方式要尽可能有获取的焦点,不然也没啥用
            //            msg := f.Tag.Get("msg")
            //            println(msg)
            //        }
            //    }
            //}

            c.JSON(200, gin.H{"msg": GetValidMsg(err, &user)})
            return
        }
        c.JSON(200, gin.H{"data": user})
        return
    })

    router.Run(":80")
}

——————————————————————————————————————————————————

4.自定义验证器/绑定器

package main

import (
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
    "reflect"
)

func _GetValidMsg(err error, obj any) string {
    getobj := reflect.TypeOf(obj)
    if errs, ok := err.(validator.ValidationErrors); ok {
        for _, e := range errs {
            if f, exist := getobj.Elem().FieldByName(e.Field()); exist {
                msg := f.Tag.Get("msg")
                return msg
            }
        }
    }
    return err.Error()
}

// 实现名为”sign“验证器的功能
func signValid(fl validator.FieldLevel) bool {
    var nameList []string = []string{"RoLingG", "Clouwer"}
    for _, namestr := range nameList {
        //为了拿值,进行断言
        name := fl.Field().Interface().(string)
        if name == namestr {
            return false
        }
    }
    //检验成功
    return true
}

// sign:不能再什么里面
type User struct {
    Name string `json:"name" binding:"required,sign" msg:"用户名校验失败"`
    Age  int    `json:"age" binding:"required" msg:"年龄校验失败"`
}

func main() {
    router := gin.Default()
    //断言
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        //自定义一个名为“sign”的验证器
        v.RegisterValidation("sign", signValid)
    }

    router.POST("/", func(c *gin.Context) {
        var user User
        err := c.ShouldBindJSON(&user)
        if err != nil {
            c.JSON(200, gin.H{"msg": _GetValidMsg(err, &user)})
            return
        }
        c.JSON(200, gin.H{"data": user})
        return
    })

    router.Run(":80")
}

实际上述代码只是结合了自定义错误信息和自定义验证器的结合例子。

自定义验证器主要是:

//断言
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    //自定义一个名为“sign”的验证器
    v.RegisterValidation("sign", signValid)
    //往下可以多添加几个自定义的检验器
    //v.RegisterValidation("uid", signValid)
    //v.RegisterValidation("uuid", signValid)
    //v.RegisterValidation("manager", signValid)
}
    
              ↑
    自定义校验器主要是这两部分
              ↓
// 实现名为”sign“验证器的功能
func signValid(fl validator.FieldLevel) bool {
    var nameList []string = []string{"RoLingG", "Clouwer"}
    for _, namestr := range nameList {
        //为了拿值,进行断言
        name := fl.Field().Interface().(string)
        if name == namestr {
            return false
        }
    }
    //检验成功
    return true
}

.

PREV
[Vue基础] Vue本人学习路程
NEXT
[Golang]Gin框架 7.文件的上传与下载

评论(0)

发布评论