validation.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package validation
  2. import (
  3. "errors"
  4. "github.com/gin-gonic/gin/binding"
  5. "github.com/go-playground/locales/en"
  6. "github.com/go-playground/locales/zh"
  7. ut "github.com/go-playground/universal-translator"
  8. "github.com/go-playground/validator/v10"
  9. zh_translations "github.com/go-playground/validator/v10/translations/zh"
  10. "golang.org/x/net/idna"
  11. "net"
  12. "reflect"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. )
  17. // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。
  18. // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。
  19. // validate 是一个单例的 validator 实例,确保全局只有一个验证器,以提高性能。
  20. // validator 实例的创建是昂贵的,因此我们使用 sync.Once 来确保它只被创建一次。
  21. var (
  22. validate *validator.Validate
  23. trans ut.Translator // 全局翻译器
  24. once sync.Once
  25. )
  26. func init() {
  27. getInstance()
  28. }
  29. // getInstance 返回一个初始化好的 validator 单例实例。
  30. func getInstance() *validator.Validate {
  31. once.Do(func() {
  32. validate = validator.New()
  33. // 注册一个函数,获取struct tag中自定义的json字段名
  34. validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
  35. name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
  36. if name == "-" {
  37. return ""
  38. }
  39. return name
  40. })
  41. // 创建翻译器
  42. en := en.New()
  43. zh := zh.New()
  44. uni := ut.New(en, zh, en) // 第一个参数是备用语言
  45. // 我们假设服务主要使用中文,所以获取中文翻译器
  46. trans, _ = uni.GetTranslator("zh")
  47. // 注册中文翻译
  48. zh_translations.RegisterDefaultTranslations(validate, trans)
  49. // 注册自定义校验
  50. registerCustomValidations(validate)
  51. // 将我们自定义的校验器与 gin 的校验器绑定
  52. binding.Validator = &defaultValidator{validate: validate}
  53. // 在这里可以注册自定义的验证函数
  54. // 例如: validate.RegisterValidation("custom_tag", customValidationFunc)
  55. })
  56. return validate
  57. }
  58. // defaultValidator 实现了 gin.Engine 的 validator 接口
  59. type defaultValidator struct {
  60. validate *validator.Validate
  61. }
  62. // ValidateStruct 接收一个对象并校验它。
  63. // ValidateStruct 接收一个对象并校验它,它会返回翻译后的错误信息。
  64. func (v *defaultValidator) ValidateStruct(obj interface{}) error {
  65. err := v.validate.Struct(obj)
  66. if err != nil {
  67. // 如果发生校验错误,则调用我们的翻译函数
  68. return translateError(err)
  69. }
  70. return nil
  71. }
  72. // Engine 返回底层的 validator 引擎
  73. func (v *defaultValidator) Engine() interface{} {
  74. return v.validate
  75. }
  76. // translateError 将 validator.ValidationErrors 翻译成更友好的中文错误信息
  77. func translateError(err error) error {
  78. var validationErrors validator.ValidationErrors
  79. if errors.As(err, &validationErrors) {
  80. var errs []string
  81. for _, e := range validationErrors {
  82. // 使用我们注册的翻译器进行翻译
  83. errs = append(errs, e.Translate(trans))
  84. }
  85. return errors.New(strings.Join(errs, "; "))
  86. }
  87. return err
  88. }
  89. // Validate 函数用于验证一个结构体的所有字段。
  90. // 它现在会返回翻译后的中文错误信息。
  91. func Validate(s interface{}) error {
  92. err := getInstance().Struct(s)
  93. if err != nil {
  94. return translateError(err)
  95. }
  96. return nil
  97. }
  98. // ValidateVar 函数用于验证单个变量。
  99. // 它现在也会返回翻译后的中文错误信息。
  100. func ValidateVar(field interface{}, tag string) error {
  101. err := getInstance().Var(field, tag)
  102. if err != nil {
  103. return translateError(err)
  104. }
  105. return nil
  106. }
  107. // registerCustomValidations 注册所有自定义的校验规则和翻译
  108. func registerCustomValidations(v *validator.Validate) {
  109. // 注册 hostport 校验器
  110. v.RegisterValidation("hostport", validateHostPort)
  111. v.RegisterValidation("idn_fqdn",isIdnFqdn)
  112. // 为 hostport 校验器注册中文翻译
  113. v.RegisterTranslation("hostport", trans, func(ut ut.Translator) error {
  114. return ut.Add("hostport", "{0} 必须是有效的 'IP:端口' 或 '域名:端口' 格式", true)
  115. }, func(ut ut.Translator, fe validator.FieldError) string {
  116. t, _ := ut.T("hostport", fe.Field())
  117. return t
  118. })
  119. v.RegisterTranslation("idn_fqdn", trans, func(ut ut.Translator) error {
  120. return ut.Add("idn_fqdn", "{0} 必须是有效的 IDN 格式", true)
  121. }, func(ut ut.Translator, fe validator.FieldError) string {
  122. t, _ := ut.T("idn_fqdn", fe.Field())
  123. return t
  124. })
  125. }
  126. // validateHostPort 是一个自定义校验函数,用于检查字符串是否为有效的 "host:port"。
  127. // 主机部分可以是 IP 地址或域名。
  128. func validateHostPort(fl validator.FieldLevel) bool {
  129. addr := fl.Field().String()
  130. // 我们可以使用 net.SplitHostPort 来检查格式。
  131. // 它能正确处理 IPv6 地址,例如 "[::1]:8080"。
  132. host, port, err := net.SplitHostPort(addr)
  133. if err != nil || host == "" || port == "" {
  134. return false // 格式不正确,或者主机或端口为空
  135. }
  136. // 检查主机部分是否为 IP 地址
  137. if net.ParseIP(host) != nil {
  138. return true
  139. }
  140. portInt, err := strconv.Atoi(port)
  141. if err != nil {
  142. return false
  143. }
  144. if portInt <= 0 || portInt > 65535 {
  145. return false
  146. }
  147. // --- 如果不是 IP,则按域名处理(这是支持中文域名的关键部分) ---
  148. // 4. 将可能为中文的域名转换为 Punycode (ASCII) 格式
  149. // 例如: "例子.com" -> "xn--fsq270a.com"
  150. asciiHost, err := idna.ToASCII(host)
  151. if err != nil {
  152. // 如果转换失败,说明主机名包含无效字符(如下划线 "_" 等),不是合法的域名
  153. return false
  154. }
  155. // 5. 使用 validator 内置的 hostname_rfc1123 规则来验证转换后的 ASCII 域名
  156. // RFC 1123 是对主机名规范的常见标准。
  157. // 注意:我们验证的是 asciiHost,而不是原始的 host。
  158. v := getInstance()
  159. err = v.Var(asciiHost, "fqdn")
  160. return err == nil
  161. }
  162. func isIdnFqdn(fl validator.FieldLevel) bool {
  163. // 获取字段的值
  164. domain := fl.Field().String()
  165. // omitempty 的行为由 validator 自身处理,
  166. // 如果字段为空,我们的函数不应该判定为 false。
  167. if domain == "" {
  168. return true
  169. }
  170. // idna.ToASCII 会将域名转换为 Punycode 格式。
  171. // 这个过程本身就包含了对 IDNA2008 标准的严格验证。
  172. // 如果域名格式不正确(例如包含非法字符、标签过长等),它会返回一个错误。
  173. // 因此,我们只需要检查这个函数是否返回错误即可。
  174. _, err := idna.ToASCII(domain)
  175. // 如果没有错误,说明它是一个有效的(可能是国际化的)域名。
  176. return err == nil
  177. }