Trong bài trước chúng ta đã tìm hiểu về cách xây dựng một web server đơn giản. Web server hoạt động là nhằm phục vụ từ client mà một trong những client phổ biến là trình duyệt. Trong bài này, chúng ta sẽ tìm hiểu cách server nhận yêu cầu từ trình duyệt và xử lý yêu cầu. Bài này gồm có 2 nội dung chính:
- Xử lý biểu mẫu (form) HTML từ client.
- Xử lý tải file lên server.
Xử lý biểu mẫu HTML
Biểu mẫu HTML rất phổ biến trong lập trình web phần front-end. Nó chứa nhiều thành phần hỗ trợ nhập liệu và hiển thị phục vụ quá trình tương tác với trang web như text box, drop down list, radio button, check box, v.v... Biểu mẫu được tạo với cặp HTML tag <form> và </form>. Để hiểu rõ hơn các xử lý biểu mẫu, chúng ta cùng khảo sát ví dụ lưu thông tin địa điểm mới như sau:
- Đầu tiên server được tạo lắng nghe cổng 8080 và chỉ xử lý yêu cầu ở đường dẫn "/" và "/locations" với hàm xử lý handler là LocationHandler:
http.HandleFunc("/", LocationHandler)
http.HandleFunc("/locations", LocationHandler)
http.ListenAndServe(":8080", nil)
- Khi truy cập địa chỉ localhost:8080/ trình duyệt sẽ hiển thị trang web nhập liệu đơn giản do server gửi về. Trong tình huống này, phương thức gọi là GET, hàm LocationHandler xử lý trường hợp này bằng cách gửi trang web nhập liệu về cho client:
if r.Method == "GET" {
var locpage string = `
<html>
<head>
<title>Địa điểm mới</title>
</head>
<body>
<h2>THÊM ĐỊA ĐIỂM MỚI</h2>
<form action="/locations" method="post">
Tên:<input type="text" name="name"><br/>
Địa chỉ:<input type="text" name="adr"><br/>
Vĩ độ:<input type="text" name="lat"><br/>
Kinh độ:<input type="text" name="lon"><br/>
Loại:<select name="type">
<option value="-">-</option>
<option value="Ăn uống">Ăn uống</option>
<option value="Nghỉ ngơi">Nghỉ ngơi</option>
<option value="Đi lại">Đi lại</option>
<option value="Giải trí">Giải trí</option>
<option value="Tiện ích">Tiện ích</option></select><br/>
<input type="submit" value="Thêm">
</form>
</body>
</html>`
fmt.Fprintf(w, locpage)
}
Biến kiểu chuỗi locpage chứa toàn bộ thông tin HTML được lưu với cặp `` để giữ nguyên định dạng (xem thêm ở bài về chuỗi). Có 5 thông tin cần nhập và nút "Thêm". Sau khi nhập liệu và nhấn nút "Thêm", dữ liệu sẽ được gửi về server với phương thức POST đến "/locations". Nếu bạn chưa biết về HTML, có thể tham khảo thêm ở đây.
- Phần xử lý tương ứng trong handler LocationHandler cho phương thức POST đảm nhận xử lý việc đọc dữ liệu từ biểu mẫu và tiến hành lưu thông tin địa điểm mới rồi phản hồi toàn bộ dữ liệu địa điểm về cho client:
else {
var location Location
// Phân tích dữ liệu gửi từ form
r.ParseForm()
location.Name = r.PostForm.Get("name")
location.Address = r.PostForm.Get("adr")
var err error
location.Latitude, err = strconv.ParseFloat(r.PostForm.Get("lat"), 32)
if err != nil {
panic(err)
}
location.Longitude, err = strconv.ParseFloat(r.PostForm.Get("lon"), 32)
if err != nil {
panic(err)
}
location.Type = r.PostForm.Get("type")
id++
locationStore[id] = location
fmt.Fprintf(w, "Thêm thành công!\nThông tin các địa điểm: %v", locationStore)
}
+ Đầu tiên biến location được tạo thuộc struct Location được khai báo như sau:
type Location struct {
Name string `json:"name"`
Address string `json:"adr"`
Latitude float64 `json:"lat"`
Longitude float64 `json:"lon"`
Type string `json:"type"`
}
+ Dữ liệu trong biểu mẫu từ client gửi lên server để có thể lấy được cần gọi phương thức ParseForm của biến http.Request r. Lúc này dữ liệu sẽ được lưu trong trường r.PostForm. PostForm có kiểu dữ liệu là url.Values: map[string][]string. Đây là map mà chúng ta có thể truy xuất giá trị dựa trên khóa là giá trị name tương ứng được khai báo ở trang web nhập thông tin ở trên. Phương thức Get của url.Values giúp chúng ta lấy chuỗi giá trị đầu tiên của phần tử map r.PostForm. Nếu một phần tử có nhiều hơn một giá trị, chúng ta buộc phải tự truy xuất map để lấy.
r.ParseForm()
location.Name = r.PostForm.Get("name")
location.Address = r.PostForm.Get("adr")
+ Vĩ độ (Latitude) và kinh độ (Longitude) là số thực nên cần dùng hàm ParseFloat chuyển từ chuỗi về Float64.
var err error
location.Latitude, err = strconv.ParseFloat(r.PostForm.Get("lat"), 32)
if err != nil {
panic(err)
}
location.Longitude, err = strconv.ParseFloat(r.PostForm.Get("lon"), 32)
if err != nil {
panic(err)
}
+ Sau khi lấy xong toàn bộ dữ liệu của location, chúng ta thêm nó vào map locationStore chứa toàn bộ các địa điểm đã khai báo trước đó là biến toàn cục:
var locationStore = make(map[int]Location)
+ Biến toàn cục nguyên id được khai báo đóng vai trò tạo khóa cho map locationStore, tăng một mỗi lần thêm địa điểm, có giá trị khởi tạo là 0.
+ Server phản hồi về client thông báo thêm địa điểm thành công và gửi về toàn bộ địa điểm đã lưu.
Xác thực dữ liệu
Nguyên tắc cơ bản khi nhận dữ liệu từ client là phải xác thực dữ liệu trước khi xử lý bởi người dùng có thể nhập liệu không đúng chuẩn hoặc dữ liệu được gửi bởi bên khác nhằm phá hoại server.
Thông thường, client sẽ xác thực dữ liệu trước bằng javascript khi xử lý trang web trên trình duyệt hay bằng hàm kiểm tra khi xử lý ứng dụng di động. Việc này chỉ giúp giảm bớt xử lý chứ không loại bỏ việc xác thực dữ liệu trên server được.
Dữ liệu bắt buộc: Thường client sẽ xử lý để đảm bảo những trường bắt buộc phải có dữ liệu trước khi gửi về server. Những dữ liệu này thường là chuỗi trong các ô nhập. Server kiểm tra bằng cách gọi hàm len(). Trong ví dụ trên, chúng ta có thể kiểm tra xem tên địa điểm có dữ liệu hay không như sau:
location.Name = r.PostForm.Get("name")
if len(strings.TrimSpace(location.Name)) == 0 {
fmt.Fprintf(w, "Thêm không thành công do tên địa điểm rỗng!\nThông tin các địa điểm: %v", locationStore)
return
}
Hàm strings.TrimSpace() sẽ loại bỏ khoảng trắng ở đầu và cuối chuỗi và nếu sau khi gọi hàm len() cho giá trị 0 thì giá trị này không chấp nhận được. Server sẽ gửi phản hồi báo lỗi ngay mà không cần xử lý tiếp.
Loại dữ liệu: Đôi khi chúng ta cần xác định dữ liệu là chữ, là số hay là email, số điện thoại trước khi xử lý. Có nhiều cách để làm việc này nhưng cách nhanh nhất là sử dụng biểu thức chính quy (regular expression). Đó là chuỗi mô tả quy tắc để xác định một chuỗi có phù hợp quy tắc đó hay không. Ví dụ: "^[0-9]+$" là biểu thức chính quy mô tả chuỗi gồm toàn các số. Chi tiết về biểu thức chính quy sẽ được đề cập sau. Go cung cấp package regexp để xử lý biểu thức chính quy. Ví dụ kiểm tra chuỗi dùng biểu thức chính quy cho vĩ độ là số thực ở ví dụ trên như sau:
if check, _ := regexp.MatchString("^([-+]?)([0-9]*)(.?)([0-9]+)$", r.PostForm.Get("lat")); !check {<Phản hồi lỗi>
}
Tương tự, áp dụng chuỗi biểu thức chính quy cho các trường hợp:
- Email: `^([\w\.\_]+)@(\w+).([a-z]{2,})$`
- Số điện thoại: "^0[0-9]{9,10}$"
Dữ liệu tồn tại: Nhiều khi dữ liệu client gửi về chỉ bó buộc trong một nhóm nhất định, thường là dữ liệu từ các thành phần drop down list, radio button hay check box. Server cần kiểm tra xem dữ liệu nhận được liệu có thuộc nhóm cho trước hay không bằng cách tạo ra slice chứa nhóm dữ liệu rồi kiểm tra xem dữ liệu client gửi về có thuộc nhóm đó hay không như sau:
loctype := []string{"Ăn uống", "Nghỉ ngơi", "Đi lại", "Giải trí", "Tiện ích"}for _, v := range loctype {
if v == r.PostForm.Get("type") {
location.Type = v
break
}
}
if len(location.Type) == 0 {
fmt.Fprintf(w, "Thêm không thành công do sai loại địa điểm!\nThông tin các địa điểm: %v", locationStore)
return
}
Dữ liệu nguy hại: Thông thường dữ liệu nhập từ biểu mẫu là chuỗi văn bản nhưng với những kẻ phá hoại, chúng sẽ nhập chuỗi mà có thể thực thi được trong một số tình huống dẫn đến nguy hại cho hoạt động trên trang web và có thể ảnh hưởng đến hoạt động của server. Thường chúng sẽ nhét các đoạn mã JavaScript, VBScript, ActiveX hay Flash, v.v... vào ô nhập và khi trang web hiển thị, thay vì hiện dữ liệu thì sẽ thực thi đoạn mã chúng mong muốn. Lỗi này được gọi là XSS (Cross Site Scripting). Để ngăn chặn chuyện này, chúng ta cần hóa giải các đoạn mã trong chuỗi. Go giúp chúng ta việc này thông qua các hàm trong package html/template như sau:
Ví dụ áp dụng vào trường tên địa điểm ở trên như sau:func HTMLEscape(w io.Writer, b []byte) // Loại bỏ ký tự gây hại từ b đổ vào wfunc HTMLEscapeString(s string) string // Trả về chuỗi mới sau khi bỏ ký tự gây hại
func HTMLEscaper(args ...interface{}) string // Như hàm trên nhưng nhận nhiều tham số đầu vào
location.Name = template.HTMLEscapeString(r.PostForm.Get("name"))
Lúc này, nếu dữ liệu nhận có script kiểu như "<script><đoạn lệnh gây hại></script> thì kết quả nhận được ở location.Name là: "<script><đoạn lệnh gây hại></script>". Chuỗi này hoàn toàn vô hại.
Tải file lên server
Một trong những dữ liệu người dùng nhập và đưa lên lưu server trong biểu mẫu là file. Để biểu mẫu có thể tải file lên cần đáp ứng 3 yêu cầu sau:
- Phương thức POST
- Enctype là "multipart/form-data"
- Loại điều khiển nhập (input type) là file
<form enctype="multipart/form-data" action="/upload" method="post">
<input type="file" name="upfile" />
<input type="submit" value="Tải lên" />
</form>
Với khai báo trên, trình duyệt sẽ tạo một nút để chọn file tải lên bằng cách mở hộp thoại chọn file khi ấn nút đó. Khi chọn nút "Tải lên", file đã chọn sẽ được gửi lên server yêu cầu xử lý tương ứng với đường dẫn "/upload" được khai báo tại thuộc tính action. Phần xử lý handler tương ứng trên server như sau:
01 r.ParseMultipartForm(32 << 20)
02 file, handler, err := r.FormFile("upfile")
03 if err != nil {
04 fmt.Fprintf(w, "Tải file lỗi!")
05 return
06 }
07 defer file.Close()
08 f, err := os.OpenFile("./images/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
09 if err != nil {
10 fmt.Fprintf(w, "Lưu file lỗi!")
11 return
12 }
13 defer f.Close()
14 io.Copy(f, file)
15 fmt.Fprintf(w, "Tải file thành công: %v", handler.Header)
- Đầu tiên, phương thức ParseMultipartForm(<Kích thước file tính theo byte>) của http.Request được gọi để đảm nhận phần nhận file tải lên. Tham số của phương thức này là kích thước file tối đa, ví dụ trên là 32 MB. Nếu file tải lên lớn hơn giá trị khai báo ở tham số này, phần thừa sẽ được lưu trong file tạm.
- Tiếp theo, đối tượng quản lý file tải lên, thông tin phần đầu file tải lên được lấy ra qua phương thức FormFile(<ID điều khiển>) như dòng 2-7. Dòng 7 cần thiết để đóng tài nguyên file khi xử lý xong.
- Dòng 8-13 tạo đối tượng quản lý file mới tạo để lưu file tải lên. File được tạo có cùng tên như file tải lên nhờ lấy thông tin qua handler.Filename. File sẽ được lưu trong thư mục images.
- Dòng 14 thực hiện copy toàn bộ nội dung từ file tải lên vào file lưu trữ. Kết quả phản hồi lưu thành công như ở dòng 15. Phần đầu yêu cầu tải lên gửi kèm về để thông tin cho client.
Một trong những vấn đề quan trọng khi tải file lên là xác thực file tải lên có đúng loại file mong muốn hay không. Ở ví dụ trên chúng ta muốn tải file hình mà cụ thể là png hoặc jpg. Việc này đòi hỏi xử lý ở cả client và server như sau:
- Client: Bổ sung thuộc tính accept cho loại điều khiển tải file như sau:
<form enctype="multipart/form-data" action="/upload" method="post"><input type="file" name="upfile" accept=".jpg,.png" />
<input type="submit" value="Tải lên" />
</form>
Lúc này khi ấn nút chọn file, hộp thoại mở ra chỉ hiển thị các file thuộc 2 loại file png và jpg để người dùng chọn. Tuy nhiên người dùng vẫn có thể chọn lại kiểu file hiển thị để hiển thị các file mà họ muốn.
- Server: Cho dù file tải lên đúng là file có đuôi .png hay .jpg thì không có gì đảm bảo chúng là file hình cả. Để đảm bảo đúng loại file mong muốn, server cần đọc phần đầu file để xác định đúng định dạng file. Package net/http cung cấp hàm DetectContentType(<slice 512 byte đầu file>) giúp chúng ta kiểm tra định dạng file. Phần xử lý này được cài đặt như sau:
01 r.ParseMultipartForm(32 << 20)
02 file, handler, err := r.FormFile("upfile")
03 if err != nil {
04 fmt.Fprintf(w, "Tải file lỗi!")
05 return
06 }
07 defer file.Close()
08
09 buff := make([]byte, 512)
10 _, err = file.Read(buff)
11 if err != nil {
12 fmt.Fprintf(w, "Đọc file lỗi!")
13 return
14 }
15
16 filetype := http.DetectContentType(buff)
17 if filetype == "image/jpeg" || filetype == "image/jpg" || filetype == "image/png" {
18 f, err := os.OpenFile("./public/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
19 if err != nil {
20 fmt.Fprintf(w, "Lưu file lỗi!")
21 return
22 }
23 defer f.Close()
24 io.Copy(f, file)
25 fmt.Fprintf(w, "Tải file thành công: %v", handler.Header)
26 } else {
27 fmt.Fprintf(w, "Định dạng file không hỗ trợ: %s", filetype)
28 }
- Phần kiểm tra bắt đầu từ dòng 9 bằng việc đọc 512 byte đầu tiên file tải lên từ đối tượng quản lý file đưa vào slice buff.
- Dòng 16 trả về loại file của file tải lên theo chuẩn MIME.
- Dòng 17-28 là phần kiểm tra đúng định dạng mong muốn thì thực hiện phần lưu file. Ngược lại báo lỗi file.
Trong bài tới chúng ta sẽ cùng tìm hiểu về xử lý văn bản trong lập trình dịch vụ web .
- Dòng 17-28 là phần kiểm tra đúng định dạng mong muốn thì thực hiện phần lưu file. Ngược lại báo lỗi file.
Trong bài tới chúng ta sẽ cùng tìm hiểu về xử lý văn bản trong lập trình dịch vụ web .
Tóm tắt:
- Biểu mẫu html: Server cần gọi phương thức ParseForm để xử lý thông tin từ biểu mẫu do client gửi lên. Dùng phương thức Form hoặc PostForm của http.Request để lấy thông tin biểu mẫu, được lưu dưới dạng map.
- Dữ liệu nhận từ client cần xác thực dữ liệu: dữ liệu bắt buộc, loại dữ liệu, dữ liệu tồn tại và dữ liệu nguy hại.
- Tải file lên server:
+ Client cần khai báo đúng loại biểu mẫu.
+ Server cần gọi ParseMultipartForm để xử lý tải file và FormFile để lấy thông tin file tải lên.
+ Server sử dụng http.DetechContentType để xác định loại file tải lại trước khi lưu.
This comment has been removed by the author.
ReplyDeleteKhông biết có dùng css được cho giao diện web ở đây ko nhỉ? Không nhìn xấu kinh dị luôn.
ReplyDeleteĐược chứ. Do ở đây mình chủ yếu nói về nội dung nên để vậy cho đơn giản thôi.
ReplyDeleteCó ví dụ demo không bạn cho xin tham khảo với.
DeleteMình định dùng Golang này để xây dựng API cho các ứng dụng khác truy xuất với CSDL Oracle, thì có ổn không bạn?
Nếu chỉ là cung cấp API thì Go dư sức làm. Nếu bạn cần render web luôn và xây dựng web service phức tạp thì nên xài web framework. Tham khảo: https://golanglibs.com/category/web-framework?sort=top
DeleteCho mình hỏi là muốn sử dụng phương thức LocationHadler như trên đầu bài thì mình khai báo như thế nào để sử dụng vậy?!
ReplyDeleteLocationHadler(w http.ResponseWriter, r *http.Request) {}
DeleteBạn đọc bài 26 nhé: https://laptrinhgo.blogspot.com/2016/09/bai-26-lap-trinh-web-voi-package-nethttp.html
ReplyDelete