In web development, you must have seen various form or interface data verification:
Client parameter verification: Before the data is submitted to the server, it occurs on the browser or app application side. Compared with server-side verification, the user experience is better and can feedback the user's input verification results in real time.
Server-side parameter verification: After the client submits data and is received by the server-side program, the server-side verification usually occurs before writing the data to the database. If the data does not pass the verification, an error message will be directly returned from the server and tell the client the specific location and reason of the error. Server-side verification does not have a good user experience like client-side verification, because it cannot return the error message until the entire form is submitted. However, server-side verification is the last line of defense for application against errors and malicious data, after which the data will be persisted to the database. All server-side frameworks today provide data verification and filtering functions (making data more secure).
This article mainly discusses server-side parameter verification
Ensure that users enter data in the correct format, and the submitted data can enable the back-end application to work normally. At the same time, under the premise that all user inputs are untrusted (such as xss cross-domain script attacks and sql injection), parameter verification is an indispensable part and a very cumbersome and inefficient link. In docking form submission or API interface data submission, the program is filled with a large number of repeated verification logic and if else statements. This article analyzes the three methods of parameter verification to find the optimal solution, thereby improving the development efficiency of parameter verification program code.
Learning method bottom-up: Ask questions -> Analyze questions -> Solve problems -> Summary
Requirement scenario:
Common website login scenarios
Business Requirements
Interface one: Scene:Enter your mobile phone number,Get the SMS verification code Verify requirements:Determine that the mobile phone number is not empty,Is the mobile phone number format correct? Interface 2: Scene:Received SMS verification code on your mobile phone,enter confirmation code,Click to log in Verify requirements:1、Determine that the mobile phone number is not empty,Is the mobile phone number format correct?;2、Verification code is not empty,Verification code format is correct
Technical selection: web framework gin
The first implementation method: customize the verification logic
package main func main() { engine := () engine := () ctrUser := () ("/user/login", ) ctrCaptcha := () ("/captcha/send", ) () } -------------------------------------------------------------------------------- package controller type Captcha struct {} func (ctr *Captcha) Send(c *) { mobile := ("mobile") // Verify mobile phone number logic if mobile == "" { (, {"error": "The mobile phone number cannot be empty"}) return } matched, _ := (`^(1[3-9][0-9]\d{8})$`, mobile) if !matched { (, {"error": "Mobile phone number format is incorrect"}) return } (, {"mobile": mobile}) } type User struct {} func (ctr *User) Login(c *) { mobile := ("mobile") code := ("code") // Verify mobile phone number logic if mobile == "" { (, {"error": "The mobile phone number cannot be empty"}) return } matched, _ := (`^(1[3-9][0-9]\d{8})$`, mobile) if !matched { (, {"error": "Mobile phone number format is incorrect"}) return } // Verify mobile phone number logic if code == "" { (, {"error": "The verification code cannot be empty"}) return } if len(code) != 4 { (, {"error": "Verification code is 4 digits"}) return } (, {"mobile": mobile, "code": code}) }
Source code link
Code analysis:
Parameter verification function is placed in the Controller layer;
This is a relatively basic and simple implementation method. It is often encountered in real code reviews. What are the problems with this implementation?
1. The logic of mobile phone number verification is repeated;
2. Violating the responsibilities of the controller layer, the controller layer is filled with a large number of verification functions (Controller layer responsibilities: obtain information from HTTP requests, extract parameters, and distribute them to different processing services);
Repeated code is a major source of software quality decline! ! !
1. Repeating code will cause an exponential increase in maintenance costs;
2. Changes in demand lead to the need to modify duplicate codes. If a duplicate logic is missed, a bug will occur (for example, adding a verification rule starting with 12 to the mobile phone number);
3. Repeating code will cause the project code to become bloated;
Smart developers must think of a solution at the first time: extract the verification logic, and implement the IsMobile function with the toolkit util
package util func IsMobile(mobile string) bool { matched, _ := (`^(1[3-9][0-9]\d{8})$`, mobile) return matched } Code Analysis: question:The code will appear in large quantities、Wait for verification code
Thinking: Starting from object-oriented thinking, does IsMobile belong to the actions or behavior of util?
The second implementation method: model binding verification
Technical selection: The model validator built into the web framework gin Chinese prompt is not very useful, so use it heregovalidatorModel binding verification is the most mainstream verification method for parameter verification at present. The web frameworks of each programming language basically support this pattern. When model binding, the data in the Http request is mapped to the corresponding parameters of the model. The parameters can be simple types, such as shaping, strings, etc., or complex types, such as Json and Json arrays, verify various data types, and then throw corresponding error messages.
Source code link
package request func init() { ["IsMobile"] = func(value string) bool { return IsMobile(value) } } func IsMobile(value string) bool { matched, _ := (`^(1[1-9][0-9]\d{8})$`, value) return matched } type Captcha struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` } type User struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` } ------------------------------------------------------------------------------- package controller type Captcha struct {} func (ctr *Captcha) Send(c *) { request := new() if err := (request); err != nil { (, {"error": ()}) return } if _, err := (request); err != nil { (, {"error": ()}) return } (, {"data": request}) } type User struct {} func (ctr *User) Login(c *) { request := new() if err := (request); err != nil { (, {"error": ()}) return } if _, err := (request); err != nil { (, {"error": ()}) return } (, {"data": request}) }
Code analysis:
1. The mobile verification logic is also repeated (the logic of the verification implementation is repeated. If the error prompts "The mobile phone number cannot be empty" and modify it to "Please fill in the mobile phone number", two places need to be modified)
2. The function will verify all properties of the structure
for2The problem is not easy to understand,Give examples Business scenarios:User registration function,Need to verify the mobile phone number、SMS verification code、password、Nick name、Birthday type User struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` Password string `form:"password" valid:"required~password不能为空,stringlength(6|18)~password6-18Characters"` Nickname string `form:"nickname" valid:"required~Nick name不能为空,stringlength(2|10)~Nick name2-10Characters"` Birthday `form:"birthday" valid:"required~Birthday不能为空" time_format:"2006-01-02"` }
Code analysis:
The login function requires verification of Mobile and Code attributes;
The registration function requires verification of Mobile, Code, Password, Nickname, and Birthday attributes;
If the code verification shares the User structure, a contradiction is created, and there are two ways to solve this problem:
- Modify the function and add a verification whitelist or blacklist to enable the verification of some attributes or ignore verification of some attributes;
// Only do Mobile and Code attribute verification or ignore Mobile and Code attribute verification(user, "Mobile", "Code") This is also a good solution,But there will be some minor problems in project practice: 1、A verification structure has20Properties,Just check it10Fields,Whether you use whitelist or blacklist, you need to pass it10Fields; 2、Handwritten field names are prone to errors;
- Create a new structure to correspond to the corresponding interface binding verification
type UserLogin struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` } type UserRegister struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` Password string `form:"password" valid:"required~Password cannot be empty,stringlength(6|18)~password6-18Characters"` Nickname string `form:"nickname" valid:"required~The nickname cannot be empty,stringlength(2|10)~Nick name2-10Characters"` Birthday `form:"birthday" valid:"required~Birthday cannot be empty" time_format:"2006-01-02"` } Code parsing: User login interface corresponds to:UserLoginStructure User registration interface corresponding:UserRegisterStructure
The same problem occurs again, and the Mobile and Code attribute verification logic is repeated.
Before introducing the third parameter verification method, let’s first examine the code just now:
if err := (&request); err != nil { (, {"error": ()}) return } if _, err := (request); err != nil { (, {"error": ()}) return }
These lines of code need to appear in the parameter binding verification. We can modify the gin source code and integrate the govalidator library into gin;
How to modify the source code reference project of third-party librarySource code link
existginAdd root directorycontext_validator.godocument,The code is as follows: package gin import ( "/asaskevich/govalidator" ) type Validator interface { Validate() error } func (c *Context) ShouldB(data interface{}) error { if err := (data); err != nil { return err } if _, err := (data); err != nil { return err } var v Validator var ok bool if v, ok = data.(Validator); !ok { return nil } return () }
The parameter binding verification code of the controller layer is as follows:
type User struct {} func (ctr *User) Register(c *) { request := new() if err := (request); err != nil { (, {"error": ()}) return } (, {"data": request}) }
Code analysis:
Added Validator interface, and the verification model implements Validator interface, which can complete more complex multi-parameter joint verification and check logic, such as checking whether the password and repeated passwords are equal.
type UserRegister struct { Mobile string `form:"mobile" valid:"required~The mobile phone number cannot be empty, numeric~The mobile phone number should be numeric, IsMobile~The mobile phone number format is wrong"` Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` Password string `form:"password" valid:"required~Password cannot be empty,stringlength(6|18)~password6-18Characters"` RePassword string `form:"rePassword" valid:"required~重复Password cannot be empty,stringlength(6|18)~重复password6-18Characters"` Nickname string `form:"nickname" valid:"required~The nickname cannot be empty,stringlength(2|10)~Nick name2-10Characters"` Birthday `form:"birthday" valid:"required~Birthday cannot be empty" time_format:"2006-01-02"` } func (req *UserRegister) Validate() error { if != { return ("The password is inconsistent twice") } return nil }
Model verification is achieved through reflection mechanism. As we all know, the efficiency of reflection is not high. Now that the gin framework integrates govalidator, the original gin verification function seems redundant. Friends can follow the ShouldBind function from the bottom, block the own verification function, and improve the efficiency of the framework.
The third implementation method: disassembly the model fields and combine the structure
The ultimate method to solve the repetition of field verification logic is to disassemble the fields into independent structures, and the required verification structure is formed through different combinations of multiple field structures. The code is as follows:
Source code link
package captcha type CodeS struct { Code string `form:"code" valid:"required~the verification code cannot be empty, numeric~the verification code should be digital"` } package user type PasswordS struct { Password string `form:"password" valid:"required~Password cannot be empty,stringlength(6|18)~password6-18Characters"` } type RePasswordS struct { RePassword string `form:"rePassword" valid:"required~重复Password cannot be empty,stringlength(6|18)~重复password6-18Characters"` } type NicknameS struct { Nickname string `form:"nickname" valid:"required~The nickname cannot be empty,stringlength(2|10)~Nick name2-10Characters"` } type BirthdayS struct { Birthday `form:"birthday" valid:"required~ Birthday cannot be empty" time_format:"2006-01-02"` } type UserLogin struct { MobileS } type UserRegister struct { MobileS } func (req *UserRegister) Validate() error { if () != () { return ("The password is inconsistent twice") } return nil } Code parsing: Why are all field structures addedS? 1、The structure contains anonymous structure and cannot call the attribute of the same name of the anonymous structure.,Anonymous structure plusSIdentified as a structure Sample code does not show the project structure well,You can view the source code
Code analysis:
- Independent field structures usually define ranges with table names as package names, such as product names and classification names field names are Name, but the verification logic (character length, etc.) required to define is likely to be different;
- Each interface establishes a corresponding verification structure:
interfaceuser/login: Corresponding request structureUserLogin interfaceuser/register: Corresponding request structureUserRegister interfacecaptcha/send: Corresponding request structureCaptchaSend
- Public field structures such as ID and Mobile create separate files;
Summarize:
1. The verification logic is encapsulated in their respective entities, and the request layer entity is responsible for the verification logic. The verification logic will not be scattered in various places in the project code. When the verification logic changes, you can find the corresponding entity to modify it. This is the high cohesion of the code;
2. Various verification requirements can be achieved through nesting combinations of different entities, which greatly enhances the reusability of the code. This is the low coupling of the code.
Independent field structures are combined into different verification structures. This method has great flexibility in actual project development and can meet the needs scenarios of relatively variable and complex parameter verification. Friends can slowly experience it in project development.
Several problems encountered in the project when parameter binding verification
Source code link1. How to verify the binding if the parameter is json or json array needs to be submitted?
type ColumnCreateArticle struct { IDS } type ColumnCreate struct { Article *ColumnCreateArticle `form:"article"` Articles []ColumnCreateArticle `form:"articles"` }
2. Strictly follow an interface corresponding to a verification structure
func (ctr *Column) Detail(c *) { request := new() if err := (request); err != nil { (, {"error": ()}) return } (, {"data": request}) }
The example code obtains the interface for the column details of the article. The parameter is the column id. Because there is only one id parameter. If you are just starting to save trouble, no corresponding independent ColumnDetail verification structure is established. If you add parameters (such as source, etc.) in the later interface, you still need to change this code to increase the uncertainty of the code.
3. Three states of boolean parameters
type ColumnDetail struct { IDS // Show key articles for true, show non-key articles for non-key articles for nil ArticleIsImportant *bool `form:"articleIsImportant"` } column?id=1&articleIsImportant=true ArticleIsImportantfortrue column?id=1&articleIsImportant=false ArticleIsImportantforfalse column?id=1 ArticleIsIm
For more information about the GO language Web parameter verification method, please see the related links below