Go高效率开发Web参数校验三种方式实例

web开发中,你肯定见到过各种各样的表单或接口数据校验:

  • 客户端参数校验:在数据提交到服务器之前,发生在浏览器端或者app应用端,相比服务器端校验,用户体验更好,能实时反馈用户的输入校验结果。
  • 服务器端参数校验:发生在客户端提交数据并被服务器端程序接收之后,通常服务器端校验都是发生在将数据写入数据库之前,如果数据没通过校验,则会直接从服务器端返回错误消息,并且告诉客户端发生错误的具体位置和原因,服务器端校验不像客户端校验那样有好的用户体验,因为它直到整个表单都提交后才能返回错误信息。但是服务器端校验是应用对抗错误,恶意数据的最后防线,在这之后,数据将被持久化至数据库。当今所有的服务端框架都提供了数据校验与过滤功能(让数据更安全)。

本文主要讨论服务器端参数校验

确保用户以正确格式输入数据,提交的数据能使后端应用程序正常工作,同时在一切用户的输入都是不可信的前提下(比如xss跨域脚本攻击,sql注入),参数验证是不可或缺的一环,也是很繁琐效率不高的一环,在对接表单提交或者api接口数据提交,程序里充斥着大量重复验证逻辑和if else语句,本文分析参数校验的三种方式,找出最优解,从而提高参数验证程序代码的开发效率。

学习方式自下而上:提出问题 -> 分析问题 -> 解决问题 -> 总结

需求场景:

常见的网站登陆场景

业务需求

接口一:
场景:输入手机号,获取短信验证码
校验需求:判断手机号非空,手机号格式是否正确
接口二:
场景:手机收到短信验证码,输入验证码,点击登陆
校验需求:1、判断手机号非空,手机号格式是否正确;2、验证码非空,验证码格式是否正确

技术选型:web框架gin

第一种实现方式:自定义实现校验逻辑

package main

func main() {
   engine := gin.New()

    engine := gin.New()

    ctrUser := controller.NewUser()
    engine.POST("/user/login", ctrUser.Login)

    ctrCaptcha := controller.NewCaptcha()
    engine.POST("/captcha/send", ctrCaptcha.Send)

    engine.Run()
}

--------------------------------------------------------------------------------
package controller

type Captcha struct {}

func (ctr *Captcha) Send(c *gin.Context) {
   mobile := c.PostForm("mobile")

   // 校验手机号逻辑
   if mobile == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
      return
   }

   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   if !matched {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
      return
   }

    c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile})
}

type User struct {}

func (ctr *User) Login(c *gin.Context) {
   mobile := c.PostForm("mobile")
   code := c.PostForm("code")

   // 校验手机号逻辑
   if mobile == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
      return
   }

   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   if !matched {
      c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
      return
   }

   // 校验手机号逻辑
   if code == "" {
      c.JSON(http.StatusBadRequest, gin.H{"error": "验证码不能为空"})
      return
   }

   if len(code) != 4 {
      c.JSON(http.StatusBadRequest, gin.H{"error": "验证码为4位"})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile, "code": code})
}

源码链接

代码分析:
参数验证函数放在Controller层;
这是一种比较初级也是最朴素的实现方式,在现实代码review中经常遇到,这样实现会有什么问题?
1、手机号码验证逻辑重复;
2、违背了controller层的职责,controller层充斥着大量的验证函数(Controller层职责:从HTTP请求中获得信息,提取参数,并分发给不同的处理服务);

重复代码是软件质量下降的重大来源!!!

1、重复代码会造成维护成本的成倍增加;
2、需求的变动导致需要修改重复代码,如果遗漏某处重复的逻辑,就会产生bug(例如手机号码增加12开头的验证规则);
3、重复代码会导致项目代码体积变得臃肿;

聪明的开发者肯定第一时间想到一个解决办法:提取出验证逻辑,工具包util实现IsMobile函数

package util

func IsMobile(mobile string) bool {
   matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
   return matched
}

代码分析:
问题:代码会大量出现util.IsMobile、util.IsEmail等校验代码

思考:从面向对象的思想出发,IsMobile属于util的动作或行为吗?

第二种实现方式:模型绑定校验

技术选型:web框架gin自带的模型验证器中文提示不是很好用,这里使用govalidator 模型绑定校验是目前参数校验最主流的验证方式,每个编程语言的web框架基本都支持这种模式,模型绑定时将Http请求中的数据映射到模型对应的参数,参数可以是简单类型,如整形,字符串等,也可以是复杂类型,如Json,Json数组,对各种数据类型进行验证,然后抛出相应的错误信息。

源码链接

package request

func init() {
   validator.TagMap["IsMobile"] = func(value string) bool {
      return IsMobile(value)
   }
}

func IsMobile(value string) bool {
    matched, _ := regexp.MatchString(`^(1[1-9][0-9]\d{8})$`, value)
    return matched
}

type Captcha struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
}

type User struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}
-------------------------------------------------------------------------------
package controller

type Captcha struct {}

func (ctr *Captcha) Send(c *gin.Context) {
   request := new(request.Captcha)
   if err := c.ShouldBind(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   if _, err := validator.ValidateStruct(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

type User struct {}

func (ctr *User) Login(c *gin.Context) {
   request := new(request.User)
   if err := c.ShouldBind(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   if _, err := validator.ValidateStruct(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

代码分析:
1、mobile校验逻辑同样重复(注释实现校验的逻辑重复,如错误提示"手机号不能为空"修改为"请填写手机号",需要修改两个地方)
2、validator.ValidateStruct函数会验证结构体所有属性

对于2问题不太好理解,举例解释
业务场景:用户注册功能,需要校验手机号、短信验证码、密码、昵称、生日
type User struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

代码分析:
登陆功能需要校验Mobile、Code属性;
注册功能需要校验Mobile、Code、Password、Nickname、Birthday属性;

如果代码校验共用User结构体,就产生了一个矛盾点,有两种方法可以解决这一问题:

  • 修改validator.ValidateStruct函数,增加校验白名单或黑名单,实现可以设置部分属性校验或者忽略校验部分属性;
// 只做Mobile、Code属性校验或者忽略Mobile、Code属性校验
validator.ValidateStruct(user, "Mobile", "Code") 

这种也是一种不错的解决方式,但是在项目实践中会遇到点小问题:
1、一个校验结构体有20个属性,只需要校验其中10个字段,不管用白名单还是黑名单都需要传10个字段;
2、手写字段名容易出错;
  • 新建不同的结构体,对应相应的接口绑定校验
type UserLogin struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}

type UserRegister struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

代码解析:
用户登陆接口对应:UserLogin结构体
用户注册接口对应:UserRegister结构体

同样问题再次出现,Mobile、Code属性校验逻辑重复。

再介绍第三种参数校验方式之前,先审视一下刚才的一段代码:

if err := c.ShouldBind(&request); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
}

if _, err := validator.ValidateStruct(request); err != nil {
  c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  return
}

参数绑定校验的地方都需要出现这几行代码,我们可以修改gin源码,把govalidator库集成在gin中;
如何修改第三方库源代码参照项目 源码链接

在gin根目录增加context_validator.go文件,代码如下:
package gin

import (
   "github.com/asaskevich/govalidator"
)

type Validator interface {
   Validate() error
}

func (c *Context) ShouldB(data interface{}) error {
   if err := c.ShouldBind(data); err != nil {
      return err
   }

   if _, err := govalidator.ValidateStruct(data); err != nil {
      return err
   }

   var v Validator
   var ok bool
   if v, ok = data.(Validator); !ok {
      return nil
   }

   return v.Validate()
}

controller层的参数绑定校验代码如下:

type User struct {}

func (ctr *User) Register(c *gin.Context) {
   request := new(request.UserRegister)
   if err := c.ShouldB(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

代码分析:
增加了Validator接口,校验模型实现Validator接口,可以完成更为复杂的多参数联合校验检查逻辑,如检查密码和重复密码是否相等

type UserRegister struct {
   Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
   RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

func (req *UserRegister) Validate() error {
   if req.Password != req.RePassword {
      return errors.New("两次密码不一致")
   }

   return nil
}

模型校验是通过反射机制来实现,众所周知反射的效率都不高,现在gin框架集成govalidator,gin原有的校验功能就显得多余,小伙伴们可以从ShouldBind函数从下追,把自带的校验功能屏蔽,提高框架效率。

第三种实现方式:拆解模型字段,组合结构体

解决字段校验逻辑重复的最终方法就是拆解字段为独立结构体,通过多个字段结构体的不同组合为所需的校验结构体,代码如下:
源码链接

package captcha

type CodeS struct {
   Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
}

package user

type PasswordS struct {
   Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
}

type RePasswordS struct {
   RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
}

type NicknameS struct {
   Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
}

type BirthdayS struct {
   Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
}

type UserLogin struct {
   MobileS
   captcha.CodeS
}

type UserRegister struct {
   MobileS
   captcha.CodeS
   user.PasswordS
   user.RePasswordS
   user.NicknameS
   user.BirthdayS
}

func (req *UserRegister) Validate() error {
   if req.Password() != req.RePassword() {
      return errors.New("两次密码不一致")
   }

   return nil
}

代码解析:
为什么字段结构体都加了S?
1、结构体包含匿名结构体不能调用匿名结构体同名属性,匿名结构体加S标识为结构体

示例代码不能很好的展示项目结构,可以查看源代码

代码分析:

  • 独立的字段结构体通常以表名为包名定义范围,比如商品名称和分类名称字段名都为Name,但是所需定义的校验逻辑(字符长度等)很有可能不同;
  • 每一个接口建立对应的验证结构体:
接口user/login:    对应请求结构体UserLogin
接口user/register: 对应请求结构体UserRegister
接口captcha/send:  对应请求结构体CaptchaSend
  • 公用的字段结构体例如ID、Mobile建立单独的文件;

总结:
一、验证逻辑封装在各自的实体中,由request层实体负责验证逻辑,验证逻辑不会散落在项目代码的各个地方,当验证逻辑改变时,找到对应的实体修改就可以了,这就是代码的高内聚;

二、通过不同实体的嵌套组合就可以实现多样的验证需求,使得代码的可重用性大大增强,这就是代码的低耦合

独立字段结构体组合成不同的校验结构体,这种方式在实际项目开发中有很大的灵活性,可以满足参数校验比较多变复杂的需求场景,小伙伴可以在项目开发中慢慢体会。

参数绑定校验在项目中遇到的几个问题

源码链接1、需要提交参数为json或json数组如何校验绑定?

type ColumnCreateArticle struct {
   IDS
   article.TitleS
}

type ColumnCreate struct {
   column.TitleS
   Article *ColumnCreateArticle `form:"article"`
   Articles []ColumnCreateArticle `form:"articles"`
}

2、严格遵循一个接口对应一个校验结构体

func (ctr *Column) Detail(c *gin.Context) {
   request := new(request.IDS)
   if err := c.ShouldB(request); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
      return
   }

   c.JSON(http.StatusBadRequest, gin.H{"data": request})
}

示例代码获取文章专栏详情的接口,参数为专栏id,因为只有一个id参数,如果刚开始图省事,没有建立对应独立的ColumnDetail校验结构体,后期接口增加参数(例如来源等),还是要改动这一块代码,增加代码的不确定性

3、布尔参数的三种状态

type ColumnDetail struct {
   IDS
   // 为真显示重点文章,为否显示非重点文章,为nil都显示
   ArticleIsImportant *bool `form:"articleIsImportant"`
}

column?id=1&articleIsImportant=true    ArticleIsImportant为true
column?id=1&articleIsImportant=false   ArticleIsImportant为false
column?id=1                            ArticleIsIm

更多关于GO语言Web参数校验方法请查看下面的相关链接

(0)

相关推荐

  • golang validator库参数校验实用技巧干货

    目录 validator库参数校验若干实用技巧 基本示例 翻译校验错误提示信息 自定义错误提示信息的字段名 自定义结构体校验方法 自定义字段校验方法 自定义翻译方法 validator库参数校验若干实用技巧 在web开发中一个不可避免的环节就是对请求参数进行校验,通常我们会在代码中定义与请求参数相对应的模型(结构体),借助模型绑定快捷地解析请求中的参数,例如 gin 框架中的Bind和ShouldBind系列方法.本文就以 gin 框架的请求参数校验为例,介绍一些validator库的实用技巧.

  • golang validator参数校验的实现

    今天在改后台页面,参数校验错误时输出全是英文,使用着很难看懂到底时什么错了 故而决定去做i18n前端国际化. 改的时候踩了很多坑,故而记录一下,顺便记录以下查问题的方式. 效果 从原来的Title is required变为标题为必填字段 完成后的代码: 这里主要定义了初始化了一个中文的trans和Validate的变量,并对其做初始化 初始化主要做了以下事情: 注册了TagName函数 // RegisterTagNameFunc registers a function to get alt

  • golan参数校验Validator

    目录 1.实践 1.1校验标签 1.2字符串约束 1.3自定义校验器 前言: 开发接口的时候需要多前端提交的参数进行参数校验,如果提交的参数只有一个两个,这样我们可以简单写个if判断,但是如果提交的参数比较多,通过if判断就比较繁琐了,在Go中有一个validator包可以通过反射结构体struct的tag进行参数校验 1.实践 go get github.com/go-playground/validator/v10 定义结体: type UserInfo struct { ID int `v

  • golang常用库之字段参数验证库-validator使用详解

    golang常用库:gorilla/mux-http路由库使用 golang常用库:配置文件解析库-viper使用 golang常用库:操作数据库的orm框架-gorm基本使用 golang常用库:字段参数验证库-validator使用 一.背景 在平常开发中,特别是在web应用开发中,为了验证输入字段的合法性,都会做一些验证操作.比如对用户提交的表单字段进行验证,或者对请求的API接口字段进行验证,验证字段的合法性,保证输入字段值的安全,防止用户的恶意请求. 一般的做法是用正则表达式,一个字段

  • Go高效率开发Web参数校验三种方式实例

    web开发中,你肯定见到过各种各样的表单或接口数据校验: 客户端参数校验:在数据提交到服务器之前,发生在浏览器端或者app应用端,相比服务器端校验,用户体验更好,能实时反馈用户的输入校验结果. 服务器端参数校验:发生在客户端提交数据并被服务器端程序接收之后,通常服务器端校验都是发生在将数据写入数据库之前,如果数据没通过校验,则会直接从服务器端返回错误消息,并且告诉客户端发生错误的具体位置和原因,服务器端校验不像客户端校验那样有好的用户体验,因为它直到整个表单都提交后才能返回错误信息.但是服务器端

  • Action获取请求参数的三种方式

    方式一:Action本身作为Model对象,通过属性注入(Setter)方法讲参数数据封装到Action中 具体为:在Action中,提供和参数名相同的几个属性,并为其提供set方法,那么,该参数会被自动封装 到该几个属性中. 方式二:创建独立的Model对象,还是通过属性注入的方法将请求数据封装到Model对象中 具体为:另外创建一个专门的类,并在其中添加几个和请求参数名相同的属性,,此时,在表单中,需要讲参数名, 写成Model的一个具体对象名.参数名的方式,然后在Action中,需要定义一

  • Android 打包三种方式实例详解

     Android 打包三种方式实例详解 前言: 现在市场上很多app应用存在于各个不同的渠道,大大小小几百个,当我们想要在发布应用之后统计各个渠道的用户下载量,我们就要进行多渠道打包. 01.应用的打包签名什么是打包? 打包就是根据签名和其他标识生成安装包. 签名是什么? 1.在android应用文件(apk)中保存的一个特别字符串 2.用来标识不同的应用开发者:开发者A,开发者B 3.一个应用开发者开发的多款应用使用同一个签名 就好比是一个人写文章,签名就相当于作者的署名. 如果两个应用都是一

  • vue-router传递参数的几种方式实例详解

    vue-router传递参数分为两大类 编程式的导航 router.push 声明式的导航 <router-link> 编程式的导航 router.push 编程式导航传递参数有两种类型:字符串.对象. 字符串 字符串的方式是直接将路由地址以字符串的方式来跳转,这种方式很简单但是不能传递参数: this.$router.push("home"); 对象 想要传递参数主要就是以对象的方式来写,分为两种方式:命名路由.查询参数,下面分别说明两种方式的用法和注意事项. 命名路由

  • 命令行运行Python脚本时传入参数的三种方式详解

    如果在运行python脚本时需要传入一些参数,例如gpus与batch_size,可以使用如下三种方式. python script.py 0,1,2 10 python script.py -gpus=0,1,2 --batch-size=10 python script.py -gpus=0,1,2 --batch_size=10 这三种格式对应不同的参数解析方式,分别为sys.argv, argparse, tf.app.run, 前两者是python自带的功能,最后一个是tensorfl

  • Spring依赖注入的三种方式实例详解

    Spring依赖注入(DI)的三种方式,分别为: 1. 接口注入 2. Setter方法注入 3. 构造方法注入 下面介绍一下这三种依赖注入在Spring中是怎么样实现的. 首先我们需要以下几个类: 接口 Logic.java 接口实现类 LogicImpl.java 一个处理类 LoginAction.java 还有一个测试类 TestMain.java Logic.java如下: package com.spring.test.di; public interface Logic { pub

  • C#实现向多线程传参的三种方式实例分析

    本文实例讲述了C#实现向多线程传参的三种方式.分享给大家供大家参考,具体如下: 从<C#高级编程>了解到给线程传递参数有两种方式,一种方式是使用带ParameterizedThreadStart委托参数的Thread构造函数,另一种方式是创建一个自定义类,把线程的方法定义为实例的方法,这样就可以初始化实例的数据,之后启动线程. 方式一:使用ParameterizedThreadStart委托 如果使用了ParameterizedThreadStart委托,线程的入口必须有一个object类型的

  • PHP连接MySQL数据库的三种方式实例分析【mysql、mysqli、pdo】

    本文实例讲述了PHP连接MySQL数据库的三种方式.分享给大家供大家参考,具体如下: PHP与MySQL的连接有三种API接口,分别是:PHP的MySQL扩展 .PHP的mysqli扩展 .PHP数据对象(PDO) ,下面针对以上三种连接方式做下总结,以备在不同场景下选出最优方案. PHP的MySQL扩展是设计开发允许php应用与MySQL数据库交互的早期扩展.MySQL扩展提供了一个面向过程的接口,并且是针对MySQL4.1.3或者更早版本设计的.因此这个扩展虽然可以与MySQL4.1.3或更

  • Java实现克隆的三种方式实例总结

    本文实例讲述了Java实现克隆的三种方式.分享给大家供大家参考,具体如下: 1.浅复制(浅克隆)这种浅复制,其实也就是把被复制的这个对象的一些变量值拿过来了.最后生成student2还是一个新的对象. public class CloneTest1 { public static void main(String[] args) throws Exception { Student1 student = new Student1(); student.setAge(24); student.se

  • PHP命令行脚本接收传入参数的三种方式

    通常PHP都做http方式请求了,可以使用GET or POST方式接收参数,有些时候需要在shell命令下把PHP当作脚本执行,比如定时任务.这就涉及到在shell命令下如何给php传参的问题,通常有三种方式传参. 一.使用$argv or $argc参数接收 复制代码 代码如下: <?php /**  * 使用 $argc $argv 接受参数  */   echo "接收到{$argc}个参数"; print_r($argv); 执行 复制代码 代码如下: [root@DE

随机推荐