123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- package validation
- import (
- "errors"
- "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"
- zh_translations "github.com/go-playground/validator/v10/translations/zh"
- "golang.org/x/net/idna"
- "net"
- "reflect"
- "strconv"
- "strings"
- "sync"
- "unicode/utf8"
- )
- // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。
- // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。
- // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。
- // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。
- var (
- validate *validator.Validate
- trans ut.Translator // 全局翻译器
- once sync.Once
- )
- func init() {
- getInstance()
- }
- // getInstance 返回一个初始化好的 validator 单例实例。
- func getInstance() *validator.Validate {
- once.Do(func() {
- validate = validator.New()
- // 注册一个函数,获取struct tag中自定义的json字段名
- validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
- name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
- if name == "-" {
- return ""
- }
- return name
- })
- // 创建翻译器
- en := en.New()
- zh := zh.New()
- uni := ut.New(en, zh, en) // 第一个参数是备用语言
- // 我们假设服务主要使用中文,所以获取中文翻译器
- trans, _ = uni.GetTranslator("zh")
- // 注册中文翻译
- zh_translations.RegisterDefaultTranslations(validate, trans)
- // 注册自定义校验
- registerCustomValidations(validate)
- // 将我们自定义的校验器与 gin 的校验器绑定
- binding.Validator = &defaultValidator{validate: validate}
- // 在这里可以注册自定义的验证函数
- // 例如: validate.RegisterValidation("custom_tag", customValidationFunc)
- })
- return validate
- }
- // defaultValidator 实现了 gin.Engine 的 validator 接口
- type defaultValidator struct {
- validate *validator.Validate
- }
- // ValidateStruct 接收一个对象并校验它。
- // ValidateStruct 接收一个对象并校验它,它会返回翻译后的错误信息。
- func (v *defaultValidator) ValidateStruct(obj interface{}) error {
- err := v.validate.Struct(obj)
- if err != nil {
- // 如果发生校验错误,则调用我们的翻译函数
- return translateError(err)
- }
- return nil
- }
- // Engine 返回底层的 validator 引擎
- func (v *defaultValidator) Engine() interface{} {
- return v.validate
- }
- // translateError 将 validator.ValidationErrors 翻译成更友好的中文错误信息
- func translateError(err error) error {
- var validationErrors validator.ValidationErrors
- if errors.As(err, &validationErrors) {
- var errs []string
- for _, e := range validationErrors {
- // 使用我们注册的翻译器进行翻译
- errs = append(errs, e.Translate(trans))
- }
- return errors.New(strings.Join(errs, "; "))
- }
- return err
- }
- // Validate 函数用于验证一个结构体的所有字段。
- // 它现在会返回翻译后的中文错误信息。
- func Validate(s interface{}) error {
- err := getInstance().Struct(s)
- if err != nil {
- return translateError(err)
- }
- return nil
- }
- // ValidateVar 函数用于验证单个变量。
- // 它现在也会返回翻译后的中文错误信息。
- func ValidateVar(field interface{}, tag string) error {
- err := getInstance().Var(field, tag)
- if err != nil {
- return translateError(err)
- }
- return nil
- }
- // registerCustomValidations 注册所有自定义的校验规则和翻译
- func registerCustomValidations(v *validator.Validate) {
- // 注册 hostport 校验器
- v.RegisterValidation("hostport", validateHostPort)
- v.RegisterValidation("idn_fqdn",isIdnFqdn)
- v.RegisterValidation("isPortStringValid", isPortStringValid)
- // 为 hostport 校验器注册中文翻译
- v.RegisterTranslation("hostport", trans, func(ut ut.Translator) error {
- return ut.Add("hostport", "{0} 必须是有效的 'IP:端口' 或 '域名:端口' 格式", true)
- }, func(ut ut.Translator, fe validator.FieldError) string {
- t, _ := ut.T("hostport", fe.Field())
- return t
- })
- v.RegisterTranslation("idn_fqdn", trans, func(ut ut.Translator) error {
- return ut.Add("idn_fqdn", "{0} 必须是有效的 IDN 格式", true)
- }, func(ut ut.Translator, fe validator.FieldError) string {
- t, _ := ut.T("idn_fqdn", fe.Field())
- return t
- })
- v.RegisterTranslation("isPortStringValid", trans, func(ut ut.Translator) error {
- return ut.Add("isPortStringValid", "{0} 必须是有效的端口号", true)
- }, func(ut ut.Translator, fe validator.FieldError) string {
- t, _ := ut.T("isPortStringValid", fe.Field())
- return t
- })
- }
- // validateHostPort 是一个自定义校验函数,用于检查字符串是否为有效的 "host:port"。
- // 主机部分可以是 IP 地址或域名。
- func validateHostPort(fl validator.FieldLevel) bool {
- addr := fl.Field().String()
- // 我们可以使用 net.SplitHostPort 来检查格式。
- // 它能正确处理 IPv6 地址,例如 "[::1]:8080"。
- host, port, err := net.SplitHostPort(addr)
- if err != nil || host == "" || port == "" {
- return false // 格式不正确,或者主机或端口为空
- }
- portInt, err := strconv.Atoi(port)
- if err != nil {
- return false
- }
- if portInt <= 0 || portInt > 65535 {
- return false
- }
- // 检查主机部分是否为 IP 地址
- if net.ParseIP(host) != nil {
- return true
- }
- // --- 如果不是 IP,则按域名处理(这是支持中文域名的关键部分) ---
- // 4. 将可能为中文的域名转换为 Punycode (ASCII) 格式
- // 例如: "例子.com" -> "xn--fsq270a.com"
- asciiHost, err := idna.ToASCII(host)
- if err != nil {
- // 如果转换失败,说明主机名包含无效字符(如下划线 "_" 等),不是合法的域名
- return false
- }
- // 5. 使用 validator 内置的 hostname_rfc1123 规则来验证转换后的 ASCII 域名
- // RFC 1123 是对主机名规范的常见标准。
- // 注意:我们验证的是 asciiHost,而不是原始的 host。
- v := getInstance()
- err = v.Var(asciiHost, "fqdn")
- return err == nil
- }
- func isIdnFqdn(fl validator.FieldLevel) bool {
- // 获取字段的值
- domain := fl.Field().String()
- // omitempty 的行为由 validator 自身处理
- if domain == "" {
- return true
- }
- // 关键步骤 1:一个有效的 FQDN 必须包含至少一个点来分隔 TLD。
- // 这会直接拦截掉 "你好世界" 这样的输入。
- if !strings.Contains(domain, ".") {
- return false
- }
- // 关键步骤 2:FQDN 不能以点开头或结尾。
- // 注意:idna.ToASCII 会检查标签是否以连字符开头/结尾,所以我们只需要检查点。
- if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") {
- return false
- }
- // 关键步骤 3:检查是否为有效的 UTF-8 字符串。
- // 这是防止非 UTF-8 编码字节流的关键。
- if !utf8.ValidString(domain) {
- return false
- }
- // 关键步骤 4:使用 idna.ToASCII 进行最严格的格式验证。
- // 它会检查:
- // - 是否包含非法字符(如 @, :, / 等)。
- // - 每个标签(点之间的部分)是否过长。
- // - 每个标签是否以连字符(-)开头或结尾。
- // - 以及其他所有 IDNA2008 标准中定义的规则。
- _, err := idna.ToASCII(domain)
- // 如果没有错误,说明它在格式上是一个有效的 FQDN。
- return err == nil
- }
- func isPortStringValid(fl validator.FieldLevel) bool {
- // 1. 获取字段的字符串值
- portStr := fl.Field().String()
- // 2. 将字符串转换为整数
- // 注意:Atoi 已经确保了字符串是纯数字,所以 'numeric' 标签其实有点冗余,但保留它有助于提前过滤和提供更清晰的错误。
- portNum, err := strconv.Atoi(portStr)
- if err != nil {
- // 如果无法转换为整数(例如,包含非数字字符),则验证失败。
- // 理论上 'numeric' 标签会先捕捉到这个错误。
- return false
- }
- // 3. 检查数值范围
- if portNum >= 1 && portNum <= 65535 {
- return true // 在有效范围内,验证通过
- }
- return false // 不在有效范围内,验证失败
- }
|