作為後端開發者我們要記住一句話:“永遠不要相信使用者的輸入”,這裡所說的使用者可能是人,也可能是另一個應用程式。“永遠不要相信使用者的輸入”是安全編碼的準則,也就是說,任何輸入的內容在驗證無害之前都是有害的。很多應用程式的安全漏洞都和使用者輸入有關,比如SQL隱碼攻擊漏洞。
我們可以通過引數驗證、sql語句過濾和引數化查詢等方式對使用者的輸入進行處理來規避這種安全隱患。本文介紹第一種方法,並對基於gin的golang web開發:模型驗證進行補充,瞭解更多的引數驗證方法。
驗證非必填的郵箱欄位
需求是這樣的:我們需要驗證一個欄位可以為空,同時欄位的值為合法的電子郵箱。
type MailRequest struct {
Email string `json:"email" binding:"email"` // 郵箱地址
}
程式碼的執行結果和我們想的不太一樣,我們沒有為欄位設定required
標籤,但是傳入空字串時會提示Email必須是一個有效的郵箱
,解決方法是加入omitempty
驗證規則。omitempty
允許條件驗證,在沒有為欄位設定值的情況下,跳過後面的驗證規則。注意omitempty
要放在其他規則前面。下面是修改後的程式碼:
type MailRequest struct {
Email string `json:"email" binding:"omitempty,email"` // 郵箱地址
}
驗證0值
先看程式碼
type AddRoleRequest struct {
Available int `json:"available" binding:"required"` // 是否可用 0 不可用 1 可用
}
Available
欄位為int
型別,新增了required
驗證規則,0為一個有效的值。Available
為0時不能通過Gin的引數驗證。這裡只需要把欄位型別修改為*int
即可。
type AddRoleRequest struct {
Available *int `json:"available" binding:"required"` // 是否可用 0 不可用 1 可用
}
自定義錯誤訊息
前文基於gin的golang web開發:模型驗證結尾部分,我們沒有把引數驗證的錯誤訊息完全翻譯成中文,欄位名還是英文的。顯然還有更優雅的做法,給使用者提示一個更友好的錯誤資訊。
{
"error": "Username為必填欄位;"
}
返回值中的Username
為欄位名稱,可以通過自定義標籤的方式修改錯誤資訊中的欄位名。我們自定義一個display標籤,然後使用標籤的值替換掉驗證器中的欄位。
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
方法也有一些改進,去掉了結果中最後一個分號。
舉個例子
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語句變為
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語句是為了方便演示,正式專案中不推薦這種寫法。完整程式碼如下:
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
}