換個語言學一下 Golang (13)——Web表單處理


 

介紹

表單是我們平常編寫Web應用常用的工具,通過表單我們可以方便的讓客戶端和服務器進 行數據的交互。對於以前開發過Web的用戶來說表單都非常熟悉。
表單是一個包含表單元素的區域。表單元素是允許用戶在表單中(比如:文本域、下拉列表、 單選框、復選框等等)輸入信息的元素。表單使用表單標簽(<form>)定義。

1、處理表單的輸入

先看一個例子吧。

注意:所有文件要放在src文件夾下
在之前文章的代碼基礎之下,創建一個view文件夾,用來存放界面的代碼文件,在view文件中創建一個login.ctpl文件,用來寫前端代碼,后綴名可根據自己喜好自定義。下面看下這個文件內的代碼:

<html>
<head>
    <title>登錄</title>
</head>
<body>
    <form action="http://localhost:9090/login" method="post">
        用戶名:<input type="text" name="username"> 
        密碼:<input type="password" name="password">
        <input type="submit" value="登陸">
    </form>
</body>
</html>

從代碼中可知,我們的界面非常簡單,就是個簡單的用戶名,密碼的表單,且通過Post方法提交到http://localhost:9090/login

然后我們再看一下這個后端對於login的處理,同樣在之前的代碼基礎上,在myserver.go文件中添加一個login的函數。
代碼如下:

package webserver

import (
    "fmt"
    "net/http"
    "text/template"
)

func login(w http.ResponseWriter, r *http.Request) {
    r.ParseForm() //解析form
    fmt.Println("method: ", r.Method)
    //判斷傳遞方式
    if r.Method == "GET" {
        //加載界面模板
        t, _ := template.ParseFiles("./view/login.ctpl")
        //將解析好的模板應用到data上,這里data為nil
        t.Execute(w, nil)
    } else if r.Method == "POST" {
        fmt.Println("username: ", r.Form["username"])
        fmt.Println("password: ", r.Form["password"])
    }
}

當代碼執行到login函數的時候,首先我們要判斷是通過什么方式傳遞過來的,是GET,還是POST。這個我們可以在r.Method 中取得,然后根據不同的方法分別處理。

在get方法中,我們調用了html/template包中的2個函數,下面看下這兩個函數的定義吧:

  • ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)

ParseFiles方法解析filenames指定的文件里的模板定義並將解析結果與t關聯。如果發生錯誤,會停止解析並返回nil,否則返回(t, nil)。至少要提供一個文件。

  • Execute
func (t *Template) Execute(wr io.Writer, data interface{}) error

Execute方法將解析好的模板應用到data上,並將輸出寫入wr。如果執行時出現錯誤,會停止執行,但有可能已經寫入wr部分數據。模板可以安全的並發執行。

在Post方法中,這里只是做了簡單的控制台打印,檢測是否跟我們輸入的一致。

好,我們的login函數寫好了,那么我們怎么調用呢?當然是路由嘍,在之前的自定義的路由中加入一個新的路由,補充myserver.go中的ServeHTTP代碼如下:

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/login" { login(w, r) return } http.NotFound(w, r) return }

然后運行,跑起來。瀏覽器地址欄中輸入:http://localhost:9090/login,結果如下:

在頁面中輸入:用戶名:chain 密碼:123456 並點擊登錄按鈕。控制台結果如下:

如圖所示,我們打印出來了我們的輸入。

request.Form是一個url.Values類型,里面存儲的是對應的類似key=value的信息,下面 展示了可以對form數據進行的一些操作:

 

v := url.Values{}
v.Set("name", "Ava")
v.Add("friend", "Jess")
v.Add("friend", "Sarah") v.Add("friend", "Zoe")
// v.Encode() == "name=Ava&friend=Jess&friend=Sarah&friend=Zoe"
fmt.Println(v.Get("name"))
fmt.Println(v.Get("friend"))
fmt.Println(v["friend"])

 

Tips: Request本身也提供了FormValue()函數來獲取用戶提交的參數。如 r.Form[“username”]也可寫成r.FormValue(“username”)。調用r.FormValue時會自動調用 r.ParseForm,所以不必提前調用。r.FormValue只會返回同名參數中的第一個,若參數不存 在則返回空字符串。

 

2、驗證表單的輸入

開發Web的一個原則就是,不能信任用戶輸入的任何信息,所以驗證和過濾用戶的輸入信 息就變得非常重要,我們經常會在微博、新聞中聽到某某網站被入侵了,存在什么漏洞,這 些大多是是因為網站對於用戶輸入的信息沒有做嚴格的驗證引起的,所以為了編寫出安全 可靠的Web程序,驗證表單輸入的意義重大。

我們平常編寫Web應用主要有兩方面的數據驗證,一個是在頁面端的js驗證(目前在這方 面有很多的插件庫,比如ValidationJS插件),一個是在服務器端的驗證,我們這小節講解 的是如何在服務器端驗證。

必填字段

通過len函數獲取表單字段長度來判斷字段是否為空:

if len(r.Form["username"][0])==0{
    //為空的處理
}

r.Form對不同類型的表單元素的留空有不同的處理, 對於空文本框、空文本區域以及文件 上傳,元素的值為空值,而如果是未選中的復選框和單選按鈕,則根本不會在r.Form中產生 相應條目,如果我們用上面例子中的方式去獲取數據時程序就會報錯。所以我們需要通過 r.Form.Get()來獲取值,因為如果字段不存在,通過該方式獲取的是空值。但是通過 r.Form.Get()只能獲取單個的值,如果是map的值,必須通過上面的方式來獲取。

數字

如果我們是判斷正整數,那么我們先轉化成int類型,然后進行處理

age, err := strconv.Atoi(r.Form.Get("age"))
if err != nil{
    fmt.Fprintf(w, "The format of the input is not correct")
}
if age < 18{
    fmt.Fprintf(w, "Minors are not registered")
}

 

下拉框,復選框,單選框

這種的校驗可以預先在服務端創建與客戶端選項一致的切片,當客戶端提交的時候再與服務端的相比較,決策出輸入是否合理。

 

正則表達式

對於一些復雜的字符串可能需要用正則表達式來匹配

if m, _ := regexp.MatchString(`表達式字符串`,
    r.Form.Get("key")); !m {
	//錯誤處理
}

下面提供幾種正則匹配規則:

  • 中文匹配
^[\\x{4e00}-\\x{9fa5}]+$
  • 英文
^[a-zA-Z]+$
  • 郵箱
`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`
  • 手機號碼
`^(1[3|4|5|8][0-9]\d{4,8})$`
  • 身份證號碼
//驗證15位身份證,15位的是全部數字
`^(\d{15})$`
//驗證18位身份證,18位前17位為數字,最后一位是校驗位,可能為數字或字符X。
`^(\d{17})([0-9]|X)$`

例子

修改下之前的login.ctpl和login函數的代碼,測試下我們的表單驗證。
login.ctpl:

<html>
<head>
    <title></title>
</head>
<body>
    <form action="http://localhost:9090/login" method="post">
        用戶名:<input type="text" name="username">
        年齡:<input type="number" name="age">
        郵箱:<input type="text" name="email">
        <input type="submit" value="按鈕">
    </form>
</body>
</html>

login函數

 

func login(w http.ResponseWriter, r *http.Request){
    r.ParseForm() //解析form
    fmt.Println("method: ", r.Method)
    if r.Method == "GET"{
        t, _ := template.ParseFiles("./view/login.ctpl")
        t.Execute(w, nil)
    }else if r.Method == "POST"{
        if len(r.Form["username"][0])==0{
            fmt.Fprintf(w, "username: null or empty \n")
        }
        age, err := strconv.Atoi(r.Form.Get("age"))
        if err != nil{
            fmt.Fprintf(w, "age: The format of the input is not correct \n")
        }
        if age < 18{
            fmt.Fprintf(w, "age: Minors are not registered \n")
        }

        if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`,
            r.Form.Get("email")); !m {
                fmt.Fprintf(w, "email: The format of the input is not correct \n")
        }
    }
}

運行結果:

3、表單文件上傳

要使表單能夠上傳文件,首先第一步就是要添加form的enctype屬性,enctype屬性有如下三種情況:

application/x-www-form-urlencoded   表示在發送前編碼所有字符(默認)
multipart/form-data   不對字符編碼。在使用包含文件上傳控件的表單時,必須使用該值。
text/plain    空格轉換為 "+" 加號,但不對特殊字符編碼。

 

下面我們基於之前的代碼基礎上,在view文件夾中創建upload.ctpl文件,用來練習文件上傳功能。其代碼如下:

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上傳文件</title>
</head>
<body>
<form enctype="multipart/form-data" action="http://127.0.0.1:9090/upload" method="post">
    <input type="file" name="uploadfile" />
    <input type="hidden" name="token" value="{{.}}"/>
    <input type="submit" value="上傳" />
</form>
</body>
</html>

從代碼中可以看到,我們的form中新增了enctype屬性,其值為multipart/form-data

然后,我們需要在myserver.go文件中增加對uplaod的處理方法:

func upload(w http.ResponseWriter, r *http.Request){
    r.ParseForm()
    if r.Method == "GET"{
        time := time.Now().Unix()
        h := md5.New()
        h.Write([]byte(strconv.FormatInt(time,10)))
        token := hex.EncodeToString(h.Sum(nil))
        t, _ := template.ParseFiles("./view/upload.ctpl")
        t.Execute(w, token)
    }else if r.Method == "POST"{
        //把上傳的文件存儲在內存和臨時文件中
        r.ParseMultipartForm(32 << 20)
        //獲取文件句柄,然后對文件進行存儲等處理
        file, handler, err := r.FormFile("uploadfile")
        if err != nil{
            fmt.Println("form file err: ", err)
            return
        }
        defer file.Close()
        fmt.Fprintf(w, "%v", handler.Header)
        //創建上傳的目的文件
        f, err := os.OpenFile("./files/" + handler.Filename, os.O_WRONLY | os.O_CREATE, 0666)
        if err != nil{
            fmt.Println("open file err: ", err)
            return
        }
        defer f.Close()
        //拷貝文件
        io.Copy(f, file)
    }
}

記得建個files文件夾

增加一條路由: 

func (p *MyMux)ServeHTTP(w http.ResponseWriter, r *http.Request){
    if r.URL.Path == "/"{
        sayHelloName(w, r)
        return
    }
    if r.URL.Path == "/about"{
        about(w, r)
        return
    }
    if r.URL.Path == "/login"{
        login(w,r)
        return
    }
    if r.URL.Path == "/upload"{
        upload(w,r)
        return
    }
    http.NotFound(w,r)
    return
}

下面我們就運行看看結果吧,瀏覽器地址欄中輸入地址:http://localhost:9090/upload,展現出以下界面:

選擇文件,並點擊upload按鈕:

點擊upload之后,結果如下:

然后我們看到我們的目錄中就多出了上傳的文件:

 

這就說明我們的上傳是成功的啊。

通過以上的代碼及結果可以看到,處理文件上傳我們需要調用r.ParseMultipartForm,里面的參數表示maxMemory,調用ParseMultipartForm之后,上傳的文件存儲在maxMemory大小的內存里面,如果文件大小超過了maxMemory,那么剩下的部分將存儲在系統的臨時文件中。我們可以通過r.FormFile獲取上面的文件句柄,然后實例中使用了io.Copy來存儲文件。

通過上面的實例我們可以看到我們上傳文件主要三步處理:

  1. 表單中增加enctype=“multipart/form-data”
  2. 服務端調用r.ParseMultipartForm,把上傳的文件存儲在內存和臨時文件中
  3. 使用r.FormFile獲取文件句柄,然后對文件進行存儲等處理。

客戶端上傳

 我們上面的例子演示了如何通過表單上傳文件,然后在服務器端處理文件,其實Go支持模擬客戶端表單功能支持文件上傳。
下面我直接在main.go文件中增加個uploadfile方法,來測試客戶端上傳的功能:

func uploadfile(filename string, url string){
    time.Sleep(30 * time.Second)
    bodyBuf := &bytes.Buffer{}
    bodyWriter :=multipart.NewWriter(bodyBuf)
    //模擬創建form表單字段
    strs := strings.Split(filename, "/")
    destname := strs[len(strs) - 1]
    filewriter, err := bodyWriter.CreateFormFile("uploadfile", destname)
    if err != nil{
        fmt.Println("error writing to buffer")
        return
    }
    //打開文件句柄操作
    fh, err := os.Open(filename)
    if err != nil{
        fmt.Println("error open file")
        return
    }
    defer fh.Close()

    //拷貝文件
    _, err = io.Copy(filewriter, fh)
    if err != nil{
        fmt.Println("error copy file")
        return
    }
    contentType := bodyWriter.FormDataContentType()
    bodyWriter.Close()

    resp, err := http.Post(url, contentType, bodyBuf)
    if err != nil{
        fmt.Println("error post buffer")
        return
    }
    defer resp.Body.Close()
    resp_body, err := ioutil.ReadAll(resp.Body)
    if err != nil{
        fmt.Println("error read all")
        return
    }
    fmt.Println(resp.Status)
    fmt.Println(string(resp_body))
}

在main函數中增加調用:

 

func main(){
    go uploadfile("d:/photo/cut.png", "http://localhost:9090/upload")
    webser.Start()
}

由於服務啟動比較耗時,我這里為了偷懶,直接讓uploadfile函數睡眠30秒再執行。這樣在服務啟動之后就可以自動用客戶端上傳文件了。運行結果如下:

 

然后再回去看下上傳目錄一樣會多出一個文件:

上面的例子詳細展示了客戶端如何向服務器上傳一個文件的例子,客戶端通過multipart.Write把文件的文本流寫入一個緩存中,然后調用http的Post方法把緩存傳到服務器。

 

4、防止多次遞交表單

 大家看了前面的代碼可能會有個疑問,那就是表單里為什么會有個token的隱藏字段呢,這個是干嘛用的呢?

不知道你是否曾經看到過一個論壇或者博客,在一個帖子或者文章后面出現多條重復的記錄,這些大多數是因為用戶重復遞交了留言的表單引起的。由於種種原因,用戶經常會重復遞交表單。通常這只是鼠標的誤操作,如雙擊了遞交按鈕,也可能是為了編輯或者再次核對填寫過的信息,點擊了瀏覽器的后退按鈕,然后又再次點擊了遞交按鈕而不是瀏覽器的前進按鈕。當然,也可能是故意的——比如,在某項在線調查或者博彩活動中重復投票。那我們如何有效的防止用戶多次遞交相同的表單呢?

解決方案是在表單中添加一個帶有唯一值的隱藏字段。在驗證表單時,先檢查帶有該惟一值的表單是否已經遞交過了。如果是,拒絕再次遞交;如果不是,則處理表單進行邏輯處理。另外,如果是采用了Ajax模式遞交表單的話,當表單遞交后,通過javascript來禁用表單的遞交按鈕。

 

源碼

github 源碼地址

轉載 Golang Web學習(14)—— 表單處理


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM