4.4 Kiểm tra tính hợp lệ của request
Một nguyên tắc quan trọng trong lập trình web là không được hoàn toàn tin những gì mà user gửi lên, luôn phải có các cơ chế xác thực, kiểm tra tính hợp lệ của các request từ client để tránh nguy cơ bảo mật, phá rối hệ thống. Từ câu chuyện xác thực request đã nảy sinh các vấn đề xung quanh khác mà chúng ta sẽ giải quyết tiếp sau đây.
Có lẽ bạn đã bắt gặp đâu đó tấm hình mà mọi người dùng để chế giễu cấu trúc của PHP:
'Hadouken' if-else
Thực tế đây là một trường hợp không liên quan gì tới ngôn ngữ mà chỉ là cách tổ chức code rườm rà khi gặp trường hợp mà nhiều field cần phải validate.
Trong phần này chúng ta sẽ dùng Go để viết một ví dụ validate và xem xét cải tiến nó theo 2 bước. Cuối cùng là phân tích cơ chế để hiểu rõ hơn cách một validator hoạt động.
4.4.1 Cải tiến 1: Tái cấu trúc hàm validation
Giả sử dữ liệu được liên kết tới một struct cụ thể thông qua binding bằng một thư viện Open source.
type RegisterReq struct {
// tag giúp json package encode giá trị của Username
// thành giá trị tương ứng với key username trong json obj
Username string `json:"username"`
PasswordNew string `json:"password_new"`
PasswordRepeat string `json:"password_repeat"`
Email string `json:"email"`
}
// register nhận vào obj kiểu RegisterReq và thực hiện validate
// các trường trong đó.
func register(req RegisterReq) error{
if len(req.Username) > 0 {
if len(req.PasswordNew) > 0 && len(req.PasswordRepeat) > 0 {
if req.PasswordNew == req.PasswordRepeat {
if emailFormatValid(req.Email) {
createUser()
return nil
} else {
return errors.New("invalid email")
}
} else {
return errors.New("password and reinput must be the same")
}
} else {
return errors.New("password and password reinput must be longer than 0")
}
} else {
return errors.New("length of username cannot be 0")
}
}
Giờ code của chúng ta có vẻ khá giống một "Hadouken" nhắc ở phần đầu rồi, vậy làm thế nào để tối ưu đoạn code trên?
Có một giải pháp đã được đưa ra trong Refactoring.com - Guard Clauses, thử áp dụng cho trường hợp của chúng ta:
func register(req RegisterReq) error{
if len(req.Username) == 0 {
return errors.New("length of username cannot be 0")
}
if len(req.PasswordNew) == 0 || len(req.PasswordRepeat) == 0 {
return errors.New("password and password reinput must be longer than 0")
}
if req.PasswordNew != req.PasswordRepeat {
return errors.New("password and reinput must be the same")
}
if emailFormatValid(req.Email) {
return errors.New("invalid email")
}
createUser()
return nil
}
Nhờ bỏ đi cách viết if-else lồng nhau mà code trở nên "clean" hơn. Tuy vậy chúng ta vẫn phải viết khá nhiều hàm validate cho mỗi field trong một kiểu request.
Có một cách giúp chúng ta giảm khá nhiều code là sử dụng validator.
4.4.2 Cải tiến 2: Sử dụng validator
Thư viện validator hỗ trợ việc validate bằng cách sử dụng các tag lúc định nghĩa struct. Một ví dụ nhỏ:
import (
"gopkg.in/go-playground/validator.v9"
"fmt"
)
// RegisterReq là struct cần được validate
type RegisterReq struct {
// gt = 0 cho biết độ dài chuỗi phải > 0,gt: greater than
Username string `json:"username" validate:"gt=0"`
PasswordNew string `json:"password_new" validate:"gt=0"`
// eqfield kiểm tra các trường bằng nhau
PasswordRepeat string `json:"password_repeat" validate:"eqfield=PasswordNew"`
// kiểm tra định dạng email thích hợp
Email string `json:"email" validate:"email"`
}
// dùng 1 instance của Validate, cache lại struct info
var validate *validator.Validate
// validatefunc để wrap hàm validate.Struct
func validatefunc(req RegisterReq) error {
err := validate.Struct(req)
if err != nil {
return err
}
return nil
}
func main() {
validate = validator.New()
// khởi tạo obj để test validator
a := RegisterReq{
Username : "Alex",
PasswordNew : "",
PasswordRepeat : "z",
Email : "z@z.z",
}
err := validatefunc(a)
fmt.Println(err)
}
// kết quả:
// Key: 'RegisterReq.PasswordNew' Error:Field validation for 'PasswordNew' failed on the 'gt' tag
// Key: 'RegisterReq.PasswordRepeat' Error:Field validation for 'PasswordRepeat' failed on the 'eqfield' tag
Một lưu ý nhỏ là error message trả về cho người dùng thì không nên viết trực tiếp bằng tiếng Anh mà thông tin về error nên được tổ chức theo từng tag để người dùng theo đó tra cứu.
4.4.3 Cơ chế của validator
Từ quan điểm cấu trúc, mỗi struct có thể được xem như một cây. Giả sử chúng ta có một struct được định nghĩa như sau:
type Nested struct {
Email string `validate:"email"`
}
type T struct {
Age int `validate:"eq=10"`
Nested Nested
}
Sẽ được vẽ thành một cây như bên dưới:
Cây validator
Việc validate các trường có thể thực hiện khi đi qua cấu trúc cây này (bằng cách duyệt theo chiều sâu hoặc theo chiều rộng). Tiếp theo chúng ta sẽ minh hoạ cơ chế validate trên một cấu trúc như thế, mục đích để hiểu rõ hơn cách mà validator thực hiện.
Đầu tiên xác định 2 struct như hình trên:
package main
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
type Nested struct {
// validate định dạng email
Email string `validate:"email"`
}
type T struct {
// chỉ cho phép age = 10
Age int `validate:"eq=10"`
Nested Nested
}
Định nghĩa hàm validate:
// validateEmail giúp xử lý các tag email
func validateEmail(input string) bool {
if pass, _ := regexp.MatchString(
`^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, input,
); pass {
return true
}
return false
}
// validate thực hiện công việc validate cho interface bất kỳ
// ở đây chỉ hiện thực cho kiểu T
func validate(v interface{}) (bool, string) {
validateResult := true
errmsg := "success"
// xác định type và value của interface input
vt := reflect.TypeOf(v)
vv := reflect.ValueOf(v)
// lần lượt duyệt trên mỗi field của struct
for i := 0; i < vv.NumField(); i++ {
// phân giải tag để áp dụng validate thích hợp
fieldVal := vv.Field(i)
tagContent := vt.Field(i).Tag.Get("validate")
k := fieldVal.Kind()
// điều kiện xét trên kiểu field của struct cần validate
switch k {
// trường hợp field là int
case reflect.Int:
// thực hiện validate cho tag eq=10
val := fieldVal.Int()
tagValStr := strings.Split(tagContent, "=")
tagVal, _ := strconv.ParseInt(tagValStr[1], 10, 64)
if val != tagVal {
errmsg = "validate int failed, tag is: "+ strconv.FormatInt(
tagVal, 10,
)
validateResult = false
}
// trường hợp field là string
case reflect.String:
val := fieldVal.String()
tagValStr := tagContent
switch tagValStr {
// nếu tag là email thì thực hiện validate tương ứng
case "email":
nestedResult := validateEmail(val)
if nestedResult == false {
errmsg = "validate mail failed, field val is: "+ val
validateResult = false
}
}
// nếu có struct lồng bên trong thì truyền
// xuống đệ quy theo chiều sâu
case reflect.Struct:
valInter := fieldVal.Interface()
nestedResult, msg := validate(valInter)
if nestedResult == false {
validateResult = false
errmsg = msg
}
}
}
return validateResult, errmsg
}
Sau đây là cách sử dụng trong hàm main:
func main() {
// khởi tạo obj để test
var a = T{Age: 10, Nested: Nested{Email: "abc@adfgom"}}
validateResult, errmsg := validate(a)
fmt.Println(validateResult, errmsg)
}
// kết quả:
// false validate mail failed, field val is: abc@adfgom
Thư viện validator được giới thiệu trong phần trước phức tạp hơn về mặt chức năng so với ví dụ ở đây. Nhưng nguyên tắc chung cũng là duyệt cây của một struct với reflection.
4.4.4 Xác thực request bằng JWT
Phần trên đã trình bày quá trình validate các thông tin về email và password khi đăng ký một tài khoản. Sau đó, nếu họ đăng nhập vào tài khoản bằng email và password thì trạng thái phiên làm việc của họ sẽ được giữ cho các yêu cầu kế tiếp. Có một số giải pháp để lưu trữ phiên làm việc bằng session/cookie, một giải pháp khác là dùng cơ chế cấp token JWT sau khi đăng nhập, và dùng token này để xác thực các yêu cầu về sau.
Không chỉ lưu trữ phiên làm việc, token JWT cũng hay đi kèm trong các lệnh gọi API để xác thực phía client khi gọi đến web service. Sau đây là một đoạn chương trình middleware xác thực yêu cầu bằng JWT:
var JwtAuthentication = func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// danh sách các API không cần xác thực bằng token
notAuth := []string{"/api/user/new", "/api/user/login"}
requestPath := r.URL.Path
for _, value := range notAuth {
if value == requestPath {
next.ServeHTTP(w, r)
return
}
}
response := make(map[string] interface{})
tokenHeader := r.Header.Get("Authorization")
// thiếu jwt token, trả về lỗi
if tokenHeader == "" {
response = u.Message(false, "Missing auth token")
w.WriteHeader(http.StatusForbidden)
w.Header().Add("Content-Type", "application/json")
u.Respond(w, response)
return
}
// thông thường chuỗi token có định dạng: Bearer {token-body}, nên cần tách phần token ra
splitted := strings.Split(tokenHeader, " ")
if len(splitted) != 2 {
response = u.Message(false, "Invalid/Malformed auth token")
w.WriteHeader(http.StatusForbidden)
w.Header().Add("Content-Type", "application/json")
u.Respond(w, response)
return
}
// chuỗi jwt token trong phần header của request
tokenPart := splitted[1]
tk := &models.Token{}
token, err := jwt.ParseWithClaims(tokenPart, tk, func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("token_password")), nil
})
if err != nil {
response = u.Message(false, "Malformed authentication token")
w.WriteHeader(http.StatusForbidden)
w.Header().Add("Content-Type", "application/json")
u.Respond(w, response)
return
}
if !token.Valid {
response = u.Message(false, "Token is not valid.")
w.WriteHeader(http.StatusForbidden)
w.Header().Add("Content-Type", "application/json")
u.Respond(w, response)
return
}
ctx := context.WithValue(r.Context(), "user", tk.UserId)
r = r.WithContext(ctx)
// tiếp tục thực hiện request
next.ServeHTTP(w, r)
});
}
Liên kết
- Phần tiếp theo: Làm việc với Database
- Phần trước: Middleware
- Mục lục