作为后端开发者我们要记住一句话:“永远不要相信用户的输入”,这里所说的用户可能是人,也可能是另一个应用程序。“永远不要相信用户的输入”是安全编码的准则,也就是说,任何输入的内容在验证无害之前都是有害的。很多应用程序的安全漏洞都和用户输入有关,比如SQL注入漏洞。
我们可以通过参数验证、sql语句过滤和参数化查询等方式对用户的输入进行处理来规避这种安全隐患。本文介绍第一种方法,并对基于gin的golang web开发:模型验证进行补充,了解更多的参数验证方法。
验证非必填的邮箱字段
需求是这样的:我们需要验证一个字段可以为空,同时字段的值为合法的电子邮箱。
1 2 3
| type MailRequest struct { Email string `json:"email" binding:"email"` // 邮箱地址 }
|
代码的执行结果和我们想的不太一样,我们没有为字段设置required标签,但是传入空字符串时会提示Email必须是一个有效的邮箱,解决方法是加入omitempty验证规则。omitempty允许条件验证,在没有为字段设置值的情况下,跳过后面的验证规则。注意omitempty要放在其他规则前面。下面是修改后的代码:
1 2 3
| type MailRequest struct { Email string `json:"email" binding:"omitempty,email"` }
|
验证0值
先看代码
1 2 3
| type AddRoleRequest struct { Available int `json:"available" binding:"required"` }
|
字段为```int```类型,添加了```required```验证规则,0为一个有效的值。```Available```为0时不能通过Gin的参数验证。这里只需要把字段类型修改为```*int```即可。1 2 3 4
| ```golang type AddRoleRequest struct { Available *int `json:"available" binding:"required"` // 是否可用 0 不可用 1 可用 }
|
自定义错误消息
前文基于gin的golang web开发:模型验证结尾部分,我们没有把参数验证的错误消息完全翻译成中文,字段名还是英文的。显然还有更优雅的做法,给用户提示一个更友好的错误信息。
1 2 3
| { "error": "Username为必填字段;" }
|
返回值中的Username为字段名称,可以通过自定义标签的方式修改错误信息中的字段名。我们自定义一个display标签,然后使用标签的值替换掉验证器中的字段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| func init() { translator := zh.New() uni = ut.New(translator, translator) trans, _ = uni.GetTranslator("zh") validate := binding.Validator.Engine().(*validator.Validate) validate.RegisterTagNameFunc(func(fld reflect.StructField) string { return fld.Tag.Get("display") }) _ = zh_translations.RegisterDefaultTranslations(validate, trans) }
func Translate(err error) string { var result string
errors := err.(validator.ValidationErrors)
for _, err := range errors { errMessage := err.Translate(trans) result += errMessage + ";" } return result[:len(result)-1] }
type AddUserRequest struct { Username string `json:"username" binding:"required" display:"用户名"` Password string `json:"password" binding:"required" display:"密码"` Nickname string `json:"nickname" binding:"required" display:"昵称"` }
|
注意代码中validate.RegisterTagNameFunc方法注册display标签,Translate方法也有一些改进,去掉了结果中最后一个分号。
举个栗子
1 2 3 4 5 6 7 8 9 10 11 12
| func checkUser(user string, password string) bool { db := GetDbContext() defer db.Close()
dataSql := ` select count(1) from sys_user where username = '` + user + `' and password = '` + password + `'` count := 0 log.Println(dataSql) db.QueryRow(dataSql).Scan(&count) return count > 0 }
|
这段代码用于判断账号密码是否正确,但是没有验证用户输入的user和password参数,恶意用户构造一个特殊的密码1' or '1'='1,dataSql中拼接的sql语句变为
1 2
| select count(1) from sys_user where username = 'xxx' and password = '1' or '1'='1'
|
查询结果大于0,方法返回真。这就造成了sql注入。我们可以在password字段上增加规则alphanum验证字段内容只能为字母或数字。1' or '1'='1不能通过参数验证也就规避掉了SQL注入的问题。例子中拼接SQL语句是为了方便演示,正式项目中不推荐这种写法。完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| type UserAndPassword struct { User string `json:"user" binding:"required,alphanum"` Pwd string `json:"pwd" binding:"required,alphanum"` }
func IsLoginIn(c *gin.Context) { var req = UserAndPassword{} if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return }
c.String(http.StatusOK, strconv.FormatBool(checkUser(req.User, req.Pwd))) }
func checkUser(user string, password string) bool { db := GetDbContext() defer db.Close()
dataSql := ` select count(1) from sys_user where username = '` + user + `' and password = '` + password + `'` count := 0 log.Println(dataSql) db.QueryRow(dataSql).Scan(&count) return count > 0 }
|