经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Go语言 » 查看文章
Go高效率开发Web参数校验三种方式实例
来源:jb51  时间:2022/11/28 8:55:49  对本文有异议

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

  • 客户端参数校验:在数据提交到服务器之前,发生在浏览器端或者app应用端,相比服务器端校验,用户体验更好,能实时反馈用户的输入校验结果。

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

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

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

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

需求场景:

常见的网站登陆场景

业务需求

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

技术选型:web框架gin

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

  1. package main
  2.  
  3. func main() {
  4. engine := gin.New()
  5.  
  6. engine := gin.New()
  7.  
  8. ctrUser := controller.NewUser()
  9. engine.POST("/user/login", ctrUser.Login)
  10.  
  11. ctrCaptcha := controller.NewCaptcha()
  12. engine.POST("/captcha/send", ctrCaptcha.Send)
  13.  
  14. engine.Run()
  15. }
  16.  
  17. --------------------------------------------------------------------------------
  18. package controller
  19.  
  20. type Captcha struct {}
  21.  
  22. func (ctr *Captcha) Send(c *gin.Context) {
  23. mobile := c.PostForm("mobile")
  24.  
  25. // 校验手机号逻辑
  26. if mobile == "" {
  27. c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
  28. return
  29. }
  30.  
  31. matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
  32. if !matched {
  33. c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
  34. return
  35. }
  36.  
  37. c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile})
  38. }
  39.  
  40. type User struct {}
  41.  
  42. func (ctr *User) Login(c *gin.Context) {
  43. mobile := c.PostForm("mobile")
  44. code := c.PostForm("code")
  45.  
  46. // 校验手机号逻辑
  47. if mobile == "" {
  48. c.JSON(http.StatusBadRequest, gin.H{"error": "手机号不能为空"})
  49. return
  50. }
  51.  
  52. matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
  53. if !matched {
  54. c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
  55. return
  56. }
  57.  
  58. // 校验手机号逻辑
  59. if code == "" {
  60. c.JSON(http.StatusBadRequest, gin.H{"error": "验证码不能为空"})
  61. return
  62. }
  63.  
  64. if len(code) != 4 {
  65. c.JSON(http.StatusBadRequest, gin.H{"error": "验证码为4位"})
  66. return
  67. }
  68.  
  69. c.JSON(http.StatusBadRequest, gin.H{"mobile": mobile, "code": code})
  70. }

源码链接

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

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

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

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

  1. package util
  2.  
  3. func IsMobile(mobile string) bool {
  4. matched, _ := regexp.MatchString(`^(1[3-9][0-9]\d{8})$`, mobile)
  5. return matched
  6. }
  7.  
  8. 代码分析:
  9. 问题:代码会大量出现util.IsMobileutil.IsEmail等校验代码

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

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

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

源码链接

  1. package request
  2.  
  3. func init() {
  4. validator.TagMap["IsMobile"] = func(value string) bool {
  5. return IsMobile(value)
  6. }
  7. }
  8.  
  9. func IsMobile(value string) bool {
  10. matched, _ := regexp.MatchString(`^(1[1-9][0-9]\d{8})$`, value)
  11. return matched
  12. }
  13.  
  14. type Captcha struct {
  15. Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
  16. }
  17.  
  18. type User struct {
  19. Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
  20. Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
  21. }
  22. -------------------------------------------------------------------------------
  23. package controller
  24.  
  25. type Captcha struct {}
  26.  
  27. func (ctr *Captcha) Send(c *gin.Context) {
  28. request := new(request.Captcha)
  29. if err := c.ShouldBind(request); err != nil {
  30. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  31. return
  32. }
  33.  
  34. if _, err := validator.ValidateStruct(request); err != nil {
  35. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  36. return
  37. }
  38.  
  39. c.JSON(http.StatusBadRequest, gin.H{"data": request})
  40. }
  41.  
  42. type User struct {}
  43.  
  44. func (ctr *User) Login(c *gin.Context) {
  45. request := new(request.User)
  46. if err := c.ShouldBind(request); err != nil {
  47. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  48. return
  49. }
  50.  
  51. if _, err := validator.ValidateStruct(request); err != nil {
  52. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  53. return
  54. }
  55.  
  56. c.JSON(http.StatusBadRequest, gin.H{"data": request})
  57. }

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

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

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

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

  • 修改validator.ValidateStruct函数,增加校验白名单或黑名单,实现可以设置部分属性校验或者忽略校验部分属性;
  1. // 只做Mobile、Code属性校验或者忽略Mobile、Code属性校验
  2. validator.ValidateStruct(user, "Mobile", "Code")
  3.  
  4. 这种也是一种不错的解决方式,但是在项目实践中会遇到点小问题:
  5. 1、一个校验结构体有20个属性,只需要校验其中10个字段,不管用白名单还是黑名单都需要传10个字段;
  6. 2、手写字段名容易出错;
  • 新建不同的结构体,对应相应的接口绑定校验
  1. type UserLogin struct {
  2. Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
  3. Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
  4. }
  5.  
  6. type UserRegister struct {
  7. Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
  8. Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
  9. Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
  10. Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
  11. Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
  12. }
  13.  
  14. 代码解析:
  15. 用户登陆接口对应:UserLogin结构体
  16. 用户注册接口对应:UserRegister结构体

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

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

  1. if err := c.ShouldBind(&request); err != nil {
  2. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  3. return
  4. }
  5.  
  6. if _, err := validator.ValidateStruct(request); err != nil {
  7. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  8. return
  9. }

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

  1. gin根目录增加context_validator.go文件,代码如下:
  2. package gin
  3.  
  4. import (
  5. "github.com/asaskevich/govalidator"
  6. )
  7.  
  8. type Validator interface {
  9. Validate() error
  10. }
  11.  
  12. func (c *Context) ShouldB(data interface{}) error {
  13. if err := c.ShouldBind(data); err != nil {
  14. return err
  15. }
  16.  
  17. if _, err := govalidator.ValidateStruct(data); err != nil {
  18. return err
  19. }
  20.  
  21. var v Validator
  22. var ok bool
  23. if v, ok = data.(Validator); !ok {
  24. return nil
  25. }
  26.  
  27. return v.Validate()
  28. }

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

  1. type User struct {}
  2.  
  3. func (ctr *User) Register(c *gin.Context) {
  4. request := new(request.UserRegister)
  5. if err := c.ShouldB(request); err != nil {
  6. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  7. return
  8. }
  9.  
  10. c.JSON(http.StatusBadRequest, gin.H{"data": request})
  11. }

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

  1. type UserRegister struct {
  2. Mobile string `form:"mobile" valid:"required~手机号不能为空,numeric~手机号码应该为数字型,IsMobile~手机号码格式错误"`
  3. Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
  4. Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
  5. RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
  6. Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
  7. Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
  8. }
  9.  
  10. func (req *UserRegister) Validate() error {
  11. if req.Password != req.RePassword {
  12. return errors.New("两次密码不一致")
  13. }
  14.  
  15. return nil
  16. }

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

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

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

  1. package captcha
  2.  
  3. type CodeS struct {
  4. Code string `form:"code" valid:"required~验证码不能为空,numeric~验证码应该为数字型"`
  5. }
  6.  
  7.  
  8. package user
  9.  
  10. type PasswordS struct {
  11. Password string `form:"password" valid:"required~密码不能为空,stringlength(6|18)~密码6-18个字符"`
  12. }
  13.  
  14. type RePasswordS struct {
  15. RePassword string `form:"rePassword" valid:"required~重复密码不能为空,stringlength(6|18)~重复密码6-18个字符"`
  16. }
  17.  
  18. type NicknameS struct {
  19. Nickname string `form:"nickname" valid:"required~昵称不能为空,stringlength(2|10)~昵称2-10个字符"`
  20. }
  21.  
  22. type BirthdayS struct {
  23. Birthday time.Time `form:"birthday" valid:"required~生日不能为空" time_format:"2006-01-02"`
  24. }
  25.  
  26. type UserLogin struct {
  27. MobileS
  28. captcha.CodeS
  29. }
  30.  
  31. type UserRegister struct {
  32. MobileS
  33. captcha.CodeS
  34. user.PasswordS
  35. user.RePasswordS
  36. user.NicknameS
  37. user.BirthdayS
  38. }
  39.  
  40. func (req *UserRegister) Validate() error {
  41. if req.Password() != req.RePassword() {
  42. return errors.New("两次密码不一致")
  43. }
  44.  
  45. return nil
  46. }
  47.  
  48. 代码解析:
  49. 为什么字段结构体都加了S
  50. 1、结构体包含匿名结构体不能调用匿名结构体同名属性,匿名结构体加S标识为结构体
  51.  
  52. 示例代码不能很好的展示项目结构,可以查看源代码

代码分析:

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

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

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

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

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

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

  1. type ColumnCreateArticle struct {
  2. IDS
  3. article.TitleS
  4. }
  5.  
  6. type ColumnCreate struct {
  7. column.TitleS
  8. Article *ColumnCreateArticle `form:"article"`
  9. Articles []ColumnCreateArticle `form:"articles"`
  10. }

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

  1. func (ctr *Column) Detail(c *gin.Context) {
  2. request := new(request.IDS)
  3. if err := c.ShouldB(request); err != nil {
  4. c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
  5. return
  6. }
  7.  
  8. c.JSON(http.StatusBadRequest, gin.H{"data": request})
  9. }

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

3、布尔参数的三种状态

  1. type ColumnDetail struct {
  2. IDS
  3. // 为真显示重点文章,为否显示非重点文章,为nil都显示
  4. ArticleIsImportant *bool `form:"articleIsImportant"`
  5. }
  6.  
  7. column?id=1&articleIsImportant=true ArticleIsImportanttrue
  8. column?id=1&articleIsImportant=false ArticleIsImportantfalse
  9. column?id=1 ArticleIsIm

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

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号