處理表單 - Go Web 開發實戰筆記

介紹

表單是編寫 Web 應用經常使用的工具,經過表單咱們能夠方便的讓客戶端和服務器進行數據的交互。表單是一個包含表單元素的區域。表單元素是容許用戶在表單中(好比:文本域、下拉列表、 單選框、複選框等等)輸入信息的元素。表單使用表單標籤(<form>)定義。javascript

獲取 HTTP Get 請求字段的值

如下是獲取 http get 請求 url 問號後的參數的示例:
GetInfo.gocss

package main

import (
    "fmt"
    "log"
    "net/http"
    "strings"
)

/*
處理表單:獲取 HTTP Get 請求字段值
*/

// GetInfoHandler 函數實現了處理器的簽名,因此這是一個處理器函數
func GetInfoHandler(w http.ResponseWriter,r *http.Request)  {
	r.ParseForm()   // get請求就解析url傳遞的參數,POST則解析響應包的主體
	fmt.Println(r.Form)
	names,ok := r.Form["name"]
	if ok == true {
    	fmt.Println(names[0])
    	fmt.Println(r.Form["age"][0])
	}

	for k,v := range r.Form {
		fmt.Println("key:",k)
		fmt.Println(v)
		fmt.Println("val:",strings.Join(v,""))
	}
	fmt.Fprintf(w,"Hello Get")
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/",GetInfoHandler)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a.html?name=Bill&age=12")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器先後輸入 http://localhost:8900/a.html?name=Bill&age=12 後,在控制檯輸出:html

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/a.html?name=Bill&age=12
map[name:[Bill] age:[12]]
Bill
12
key: name
[Bill]
val: Bill
key: age
[12]
val: 12
複製代碼

瀏覽器訪問的網頁顯示:Hello Getjava

用表單提交用戶登陸信息

如下是 Web 表單登陸的例子:
建立一個 login.tpl 文件,代碼以下:git

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
</head>
<body>
<form action="/login" method="post">
    <p>用戶名:<input type="text" name="username" /></p>
    <p>密  碼:<input type="password" name="password" /></p>
    <p><input type="submit" value="登陸" /></p>
</form>
</body>
</html>
複製代碼

以上表單代碼實現輸入用戶名和密碼點擊登陸後,客戶端就會以 post 請求方式提交表單數據到服務器地址 http://localhost:8900/logingithub

頁面預覽:web

服務端文件 PostRequest.go,代碼以下:瀏覽器

package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
)

/*
用表單提交用戶登陸信息(POST 請求)
*/

func LoginHandler(w http.ResponseWriter, r *http.Request)  {
	r.ParseForm() // 解析url傳遞的參數,對於POST則解析響應包的主體(request body)
	fmt.Println(r.Form) // 輸出到服務器端的打印信息
	fmt.Println("method:", r.Method) // 獲取請求的方法
	if r.Method == "GET" {
		// 顯示靜態登陸頁面
		t, _ := template.ParseFiles("/Users/play/goweb/src/form/login.tpl")
		t.Execute(w, nil)
	} else {
		// 請求的是登陸數據,那麼執行登陸的邏輯判斷
		username := r.Form["username"][0]
		password := r.Form["password"][0]
		if username == "Bill" && password == "123456" {
			fmt.Fprintf(w,"登陸成功")
		} else {
			fmt.Fprintf(w, "登陸失敗")
		}
	}
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/login",LoginHandler)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/login")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

以上服務器代碼實現用戶輸入用戶名:Bill、密碼:123456 時登陸成功,不然登陸失敗。安全

執行以上程序後,在瀏覽器表單頁面輸入用戶名:Bill、密碼:123456 點擊登陸後,頁面提示登陸成功;若是輸入其它,如用戶名:aa、密碼:1234 則跳轉頁面後提示登陸失敗。
執行完以上操做後,服務器控制檯輸出:bash

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/login
map[]
method: GET
map[username:[Bill] password:[123456]]
method: POST
map[]
method: GET
map[username:[aa] password:[1234]]
method: POST
複製代碼

驗證表單的輸入

開發 Web 的一個原則就是,不能信任用戶輸入的任何信息,驗證和過濾用戶的輸入信息很是必要。日常編寫 Web 應用主要有兩方面的數據驗證,一個是在頁面端的 js 驗證,一個是在服務器端的驗證。

如下示例實現的是服務器端的表單元素驗:

建立一個 form.tpl 文件,代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>驗證表單</title>
    <style type="text/css">
        body {
            padding: 10px 60px;
        }
        dl,dt,dd {
            padding: 0;
            margin: 0;
        }
        dl {
            display: block;
            clear: both;
            overflow: auto;
            margin-bottom: 10px;
        }
        dt,dd {
            float: left;
            color: #333333;
            font-size: 14px;
        }
        dt {
            width: 88px;
        }
        dd input[type="text"] {
            width: 200px;
        }
        p input {
            width: 100px;
            margin-left: 100px;
        }
        i {
            font-size: 12px;
            color: #cccccc;
        }
    </style>
</head>
<body>
<form action="/verifyForm" method="post">
    <dl>
        <dt>用戶名:</dt>
        <dd>
            <input type="text" name="username" />
            <i>必填字段</i>
        </dd>
    </dl>

    <dl>
        <dt>年齡:</dt>
        <dd>
            <input type="text" name="age" />
            <i>數字</i>
        </dd>
    </dl>

    <dl>
        <dt>真實姓名:</dt>
        <dd>
            <input type="text" name="realname" />
            <i>中文</i>
        </dd>
    </dl>

    <dl>
        <dt>英文名:</dt>
        <dd>
            <input type="text" name="engname" />
            <i>英文</i>
        </dd>
    </dl>

    <dl>
        <dt>郵箱地址:</dt>
        <dd>
            <input type="text" name="email" />
            <i>電子郵件地址</i>
        </dd>
    </dl>

    <dl>
        <dt>手機號碼:</dt>
        <dd>
            <input type="text" name="mobile" />
            <i>手機號碼</i>
        </dd>
    </dl>

    <dl>
        <dt>水果:</dt>
        <dd>
            <select name="fruit">
                <option value="apple">apple</option>
                <option value="pear">pear</option>
                <option value="banane">banane</option>
            </select>
            <i>下拉菜單</i>
        </dd>
    </dl>

    <dl>
        <dt>性別:</dt>
        <dd>
            <label><input type="radio" name="gender" value="1">男</label>
            <label><input type="radio" name="gender" value="2">女</label>
            <i>單選按鈕</i>
        </dd>
    </dl>

    <dl>
        <dt>愛好:</dt>
        <dd>
            <label><input type="checkbox" name="interest" value="football">足球</label>
                <label><input type="checkbox" name="interest" value="basketball">籃球</label>
                    <label><input type="checkbox" name="interest" value="tennis">網球</label>
            <i>複選框</i>
        </dd>
    </dl>

    <dl>
        <dt>身份證號:</dt>
        <dd>
            <input type="text" name="usercard" />
            <i>身份證號碼</i>
        </dd>
    </dl>
    <p><input type="submit" value="提交表單"></p>
    </form>
</body>
</html>
複製代碼

以上表單代碼實現輸入各項點擊提交表單後,客戶端就會以 post 請求方式提交表單數據到服務器地址 http://localhost:8900/verifyForm
頁面預覽:

服務端文件 VerifyForm.go,代碼以下:

package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
	"regexp"
	"strconv"
)

func VerifyFormHandler(w http.ResponseWriter, r *http.Request)  {
	r.ParseForm()	// 分析客戶端的body數據

	fmt.Println(r.Form)	// 輸出到服務器端的打印信息

	fmt.Println("method:", r.Method) // 獲取請求的方法
	if r.Method == "GET" {
		// 顯示靜態頁面
		t, _ := template.ParseFiles("/Users/play/goweb/src/form/form.tpl")
		t.Execute(w, nil)
	} else {

		// 必填字段
		if len(r.Form["username"][0])==0{
			// 爲空的處理
			fmt.Fprintf(w,"用戶名不能爲空")
			return
		}

		// 數字
		if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
			fmt.Fprintf(w,"年齡必須是正數")
			return
		}

		// 中文
		if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
			fmt.Fprintf(w,"真實姓名必須是中文")
			return
		}

		// 英文
		if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
			fmt.Fprintf(w,"英文名必須是英文")
			return
		}

		// 電子郵件地址
		if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
			fmt.Fprintf(w,"郵箱無效")
			return
		}

		// 手機號碼
		if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
			fmt.Fprintf(w,"手機號無效")
			return
		}

		// 下拉菜單
		slice := []string{"apple","pear","banane"}
		hasfruit := false
		for _, v := range slice {
			if v == r.Form.Get("fruit") {
				hasfruit = true
			}
		}
		if !hasfruit {
			fmt.Fprintf(w,"下拉菜單選項不存在")
			return
		}

		// 單選按鈕
		slice2:=[]int{1,2}
		hasgender := false
		for _, v2 := range slice2 {
			getint,_:=strconv.Atoi(r.Form.Get("gender"))
			if v2 == getint {
				hasgender = true
			}
		}
		if !hasgender {
			fmt.Fprintf(w,"性別選項有誤")
			return
		}

		// 複選框
		slice3 := []string{"football","basketball","tennis"}
		hasinterest := false
		for _, v3 := range slice3 {
			if v3 == r.Form.Get("interest") {
				hasinterest = true
			}
		}
		if !hasinterest {
			fmt.Fprintf(w,"愛好選項有誤")
			return
		}

		// 身份證號碼
		isusercard := false
		usercard := r.Form.Get("usercard")

		// 驗證15位身份證,15位的是所有數字
		m1,_ := regexp.MatchString(`^(\d{15})$`, usercard)

		// 驗證18位身份證,18位前17位爲數字,最後一位是校驗位,可能爲數字或字符X。
		m2,_ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, usercard)

		if m1 || m2 {
			isusercard = true
		}

		if (!isusercard) {
			fmt.Fprintf(w,"身份證號有誤")
			return
		}

		fmt.Fprintf(w,"表單驗證經過")

	}
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/verifyForm",VerifyFormHandler)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/verifyForm")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/verifyForm 訪問驗證表單頁面,服務器控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/verifyForm
map[]
method: GET
複製代碼

以上服務器端的表單元素驗證,若是所有驗證經過,則頁面提示表單驗證經過,不然頁面會提示相關驗證不經過項。

總結:

  • 必填字段
    經過內置函數 len 來獲取數據的長度來驗證是否爲必填字段,例如:

    if len(r.Form["username"][0])==0{
        //爲空的處理
    }
    複製代碼

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

  • 數字
    判斷一個表單輸入框中輸入的是不是正整數,須要先轉化成 int 類型,而後進行處理:

    getint,err:=strconv.Atoi(r.Form.Get("age"))
    if err!=nil{
        //數字轉化出錯了,那麼可能就不是數字
    }
    
    還有一種方式就是正則匹配的方式
    if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {
        return false
    }
    複製代碼
  • 中文
    驗證是不是中文,可使用 unicode 包提供的 func Is(rangeTab *RangeTable, r rune) bool 來驗證,也可使用正則方式來驗證,這裏使用最簡單的正則方式,以下代碼所示:

    if m, _ := regexp.MatchString("^\\p{Han}+$", r.Form.Get("realname")); !m {
        return false
    }
    複製代碼
  • 英文
    驗證是不是英文,能夠很簡單的經過正則驗證數據:

    if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("engname")); !m {
        return false
    }
    複製代碼
  • 電子郵件地址
    驗證 Email 地址是否正確,經過以下方式:

    if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {
        fmt.Println("no")
    }else{
        fmt.Println("yes")
    }
    複製代碼
  • 手機號碼
    經過正則方式驗證手機號碼是否正確:

    if m, _ := regexp.MatchString(`^(1[3|4|5|8][0-9]\d{4,8})$`, r.Form.Get("mobile")); !m {
        return false
    }
    複製代碼
  • 下拉菜單
    若是想要判斷表單裏面元素生成的下拉菜單中是否有被選中的項目。有些時候黑客可能會僞造這個下拉菜單不存在的值發送給你,那麼如何判斷這個值是不是咱們預設的值呢?
    select 多是這樣的一些元素

    <select name="fruit">
        <option value="apple">apple</option>
        <option value="pear">pear</option>
        <option value="banane">banane</option>
    </select>
    複製代碼

    那麼能夠這樣來驗證

    slice:=[]string{"apple","pear","banane"}
    
    for _, v := range slice {
        if v == r.Form.Get("fruit") {
            return true
        }
    }
    return false
    複製代碼
  • 單選按鈕
    若是想要判斷 radio 按鈕獲取的值是咱們預設的值,而不是黑客可能會僞造的額外的值。
    radio 多是這樣的一些元素

    <input type="radio" name="gender" value="1">男
    <input type="radio" name="gender" value="2">女
    複製代碼

    那麼能夠這樣來驗證

    slice:=[]int{1,2}
    
    for _, v := range slice {
        if v == r.Form.Get("gender") {
            return true
        }
    }
    return false
    複製代碼
  • 複選框
    有一項選擇興趣的複選框,你想肯定用戶選中的和你提供給用戶選擇的是同一個類型的數據。

    <input type="checkbox" name="interest" value="football">足球
    <input type="checkbox" name="interest" value="basketball">籃球
    <input type="checkbox" name="interest" value="tennis">網球
    複製代碼

    對於複選框咱們的驗證和單選有點不同,由於接收到的數據是一個slice

    slice:=[]string{"football","basketball","tennis"}
    a:=Slice_diff(r.Form["interest"],slice)
    if a == nil{
        return true
    }
    
    return false
    複製代碼

    上面這個函數 Slice_diff 包含在我開源的一個庫裏面(操做slice和map的庫),github.com/astaxie/bee…

  • 身份證號碼
    若是想驗證表單輸入的是不是身份證,經過正則也能夠方便的驗證,可是身份證有15位和18位,兩個都須要驗證

    //驗證15位身份證,15位的是所有數字
    if m, _ := regexp.MatchString(`^(\d{15})$`, r.Form.Get("usercard")); !m {
        return false
    }
    
    //驗證18位身份證,18位前17位爲數字,最後一位是校驗位,可能爲數字或字符X。
    if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {
        return false
    }
    複製代碼

防止跨站腳本

動態站點會受到一種名爲「跨站腳本攻擊」(Cross Site Scripting, 安全專家們一般將其縮寫成 XSS)的威脅,而靜態站點則徹底不受其影響。

攻擊者一般會在有漏洞的程序中插入 JavaScript、VBScript、 ActiveX 或 Flash 以欺騙用戶。一旦得手,他們能夠盜取用戶賬戶信息,修改用戶設置,盜取/污染 cookie 和植入惡意廣告等。

對 XSS 最佳的防禦應該結合如下兩種方法:一是驗證全部輸入數據,有效檢測攻擊;另外一個是對全部輸出數據進行適當的處理,以防止任何已成功注入的腳本在瀏覽器端運行。

如下示例實現的是服務器端對全部輸出數據進行適當的處理:

建立一個 xss.tpl 文件,代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>預防跨站腳本</title>
    <style type="text/css">
        body {
            padding: 10px 20px;
        }
        dl,dt,dd {
            padding: 0;
            margin: 0;
        }
        dt,dd {
            color: #333333;
            font-size: 14px;
        }
        p input {
            width: 100px;
        }

    </style>
</head>
<body>
<form action="/xss" method="post">
    <dl>
        <dt>正文:</dt>
        <dd>
            <textarea style="width:400px; height:200px" name="code"></textarea>
        </dd>
    </dl>
    <p><input type="submit" value="提交"></p>
    </form>
</body>
</html>
複製代碼

以上表單代碼實現輸入各項點擊提交後,客戶端就會以post請求方式提交表單數據到服務器地址 http://localhost:8900/xss
頁面預覽:

服務端文件 XssRequest.go,代碼以下:

package main

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

func XssHandler(w http.ResponseWriter, r *http.Request)  {
	r.ParseForm()	// 分析客戶端的body數據
	if r.Method == "GET" {
		// 顯示靜態頁面
		t, _ := template.ParseFiles("/Users/play/goweb/src/form/xss.tpl")
		t.Execute(w, nil)
	} else {
		code := r.Form.Get("code")
		fmt.Println(code)
		reg,_ := regexp.Compile(`<script[^>]*> </script>`)
		text := reg.ReplaceAllLiteralString(code,"")
		fmt.Println(code)
		t,_ := template.New("test").Parse(`<html>{{ . }}</html>`)
		t.ExecuteTemplate(w,"test", text)
	}
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/xss",XssHandler)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/xss")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/xss 訪問預防跨站腳本頁面,服務器控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/xss
複製代碼

在預防跨站腳本頁面文本框輸入:

hello world
<script type="text/javascript">alert("hello world")</script>
<h1>hello world</h1>
複製代碼

而後點擊提交,頁面顯示

服務器控制檯輸出:

hello world
<script type="text/javascript">alert("hello world")</script>
<h1>hello world</h1>
hello world
<script type="text/javascript">alert("hello world")</script>
<h1>hello world</h1>

複製代碼

以上是使用正則表達的包 regexp.Compile 替換實現預防跨站腳本,Go 的 html/template 裏面帶有下面幾個函數能夠實現轉義,以達到有效防禦。

  • func HTMLEscape(w io.Writer, b []byte) // 把 b 進行轉義以後寫到 w
  • func HTMLEscapeString(s string) string // 轉義 s 以後返回結果字符串
  • func HTMLEscaper(args ...interface{}) string // 支持多個參數一塊兒轉義,返回結果字符串

示例:

fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //輸出到服務器端
template.HTMLEscape(w, []byte(r.Form.Get("username"))) //輸出到客戶端
複製代碼

若是輸入的 username 是 alert() ,那麼在瀏覽器上面看到輸出以下所示:

&lt;script&gt;alert()&lt;/script&gt;
複製代碼

Go 的 html/template 包默認幫你過濾了 html 標籤,可是有時候只想要輸出這個 alert() 看起來正常的信息,該怎麼處理?請使用 text/template。請看下面的例子:

import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
複製代碼

輸出

Hello, <script>alert('you have been pwned')</script>!
複製代碼

或者使用 template.HTML 類型

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", template.HTML("<script>alert('you have been pwned')</script>"))
複製代碼

輸出

Hello, <script>alert('you have been pwned')</script>!
複製代碼

轉換成 template.HTML 後,變量的內容也不會被轉義
轉義的例子:

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
複製代碼

轉義以後的輸出:

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
複製代碼

防止屢次重複提交表單

因爲種種緣由,用戶常常會重複提交表單。所以開發表單功能時須要採起方案有效的防止用戶屢次提交相同的表單。

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

建立一個 repeatpost.tpl 文件,代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防止重複提交表單</title>
    <style type="text/css">
        body {
            padding: 10px 20px;
        }
        dl,dt,dd {
            padding: 0;
            margin: 0;
        }
        dt,dd {
            color: #333333;
            font-size: 14px;
        }
        p input {
            width: 100px;
        }

    </style>
</head>
<body>
<form action="/repeatpost" method="post">
    <dl>
        <dt>正文:</dt>
        <dd>
            <textarea style="width:400px; height:200px" name="code"></textarea>
            <input type="hidden" name="token" value="{{ . }}"
        </dd>
    </dl>
    <p><input type="submit" value="提交"></p>
    </form>
</body>
</html>
複製代碼

以上 web 頁面,在表單代碼放置一個隱藏字段(`<input type="hidden" name="token" value="{{ . }}"``),頁面每次加載後,服務端都會爲其生成一個惟一的 Token。若是發現 Token 重複提交,那麼就不會處理提交上來的數據,會直接報錯。

頁面預覽:

服務端文件 repeatpost.go,代碼以下:

package main

import (
	"crypto/sha512"
	"fmt"
	"io"
	"log"
	"net/http"
	"regexp"
	"strconv"
	"text/template"
	"time"
)

/*
防止屢次重複提交表單(POST 請求)
*/

var count = 0
func repeatpostHandler(w http.ResponseWriter, r *http.Request)  {
	r.ParseForm()	// 分析客戶端的body數據
	if r.Method == "GET" {
		// 生成 Token
		currentTime := time.Now().Unix()
		fmt.Println("當前時間戳:", currentTime)
		h := sha512.New()
		io.WriteString(h,strconv.FormatInt(currentTime,10))
		token := fmt.Sprintf("%x",h.Sum(nil))
		fmt.Println("token:", token)

		// 經過模版裝載頁面
		t, _ := template.ParseFiles("/Users/play/goweb/src/form/repeatpost.tpl")
		t.Execute(w, token)
	} else {
		token := r.Form.Get("token")
		if token != "" {
			fmt.Println("校驗 Token,持有該 Token 的頁面只能提交一次")
			if count > 0 {
				fmt.Fprintln(w, "您重複提交了")
				return
			}
			fmt.Println(count)
			count++
		} else {
			fmt.Fprintf(w,"Token 不存在")
		}

		code := r.Form.Get("code")
		reg,_ := regexp.Compile(`<script[^>]*> </script>`)
		text := reg.ReplaceAllLiteralString(code,"")
		t,_ := template.New("test").Parse(`<html>{{ . }}</html>`)
		t.ExecuteTemplate(w,"test", text)
	}
}

func main()  {
	// 註冊路由和路由函數,將url規則與處理器函數綁定作一個map映射存起來,而且會實現ServeHTTP方法,使處理器函數變成Handler函數
	http.HandleFunc("/repeatpost",repeatpostHandler)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/repeatpost")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/repeatpost 訪問防止重複提交表單頁面,服務器控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/repeatpost
當前時間戳: 1562831164
token: b450ca0e5ffb9e17e23931a864e2ed34e597515987422867e656f2a06eda8d213537633ff1ac87e3d5f80d685ba8a1f5471f88a7c5757d7c35d99369cf11bc60
複製代碼

此時,在瀏覽器查看防止重複提交表單頁面源碼,<input />組件 token 隱藏域有值。若是不斷的刷新頁面,能夠看到這個值在不斷的變化,以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>防止重複提交表單</title>
    <style type="text/css">
        body {
            padding: 10px 20px;
        }
        dl,dt,dd {
            padding: 0;
            margin: 0;
        }
        dt,dd {
            color: #333333;
            font-size: 14px;
        }
        p input {
            width: 100px;
        }

    </style>
</head>
<body>
<form action="/repeatpost" method="post">
    <dl>
        <dt>正文:</dt>
        <dd>
            <textarea style="width:400px; height:200px" name="code"></textarea>
            <input type="hidden" name="token" value="b450ca0e5ffb9e17e23931a864e2ed34e597515987422867e656f2a06eda8d213537633ff1ac87e3d5f80d685ba8a1f5471f88a7c5757d7c35d99369cf11bc60"
        </dd>
    </dl>
    <p><input type="submit" value="提交"></p>
    </form>
</body>
</html>
複製代碼

這樣就保證了每次顯示 form 表單都是惟一的,用戶遞交的表單保持了惟一性。

以上解決方案能夠防止非惡意的攻擊,並能使惡意用戶暫時不知所措,然而,它卻不能排除全部的欺騙性的動機,對此類狀況還須要更復雜的工做。

文件上傳

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

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

建立一個 uploadFile.tpl 文件,代碼以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>上傳文件</title>
    <style type="text/css">
        body {
            padding: 10px 20px;
        }
        dl,dt,dd {
            padding: 0;
            margin: 0;
        }
        dt,dd {
            color: #333333;
            font-size: 14px;
        }
    </style>
</head>
<body>
<form enctype="multipart/form-data" action="/upload" method="post">
    <dl>
        <dt>上傳文件:</dt>
        <dd>
            <input type="file" name="uploadfile">
        </dd>
    </dl>
    <p><input type="submit" value="提交"></p>
    </form>
</body>
</html>
複製代碼

頁面預覽:

服務端文件 uploadFile.go,代碼以下:

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"text/template"
)

/*
上傳文件通過以下 3 步:
1. 在 Web 頁面選擇一個文件,而後上傳
2. 在服務端讀取上傳文件的數據(字節流
3. 將文件數據寫到服務端的某一個文件中
*/

func uploadFileHandle(w http.ResponseWriter, r *http.Request)  {
	r.ParseMultipartForm(1024*1024)	// 最多在內存中一次處理 1 MB 的數據
	file, handler,err := r.FormFile("uploadfile")

	if err != nil {
		fmt.Println(err)
		return
	}

	defer file.Close()  // 延遲關閉文件(在uploadFile函數結束時關閉文件)
	fmt.Fprintf(w, "%v", handler.Header)

	// 打開服務器端文件
	f, err := os.OpenFile("./upload/" + handler.Filename, os.O_WRONLY | os.O_CREATE, 0666)

	if err != nil {
		fmt.Println(err)
		return
	}
	defer f.Close()
	io.Copy(f, file)
}

func showUploadfilePage(w http.ResponseWriter, r *http.Request)  {
	if r.Method == "GET" {
		t,_ := template.ParseFiles("/Users/play/goweb/src/form/uploadFile.tpl")
		t.Execute(w,nil)
	}
}

func main()  {
	http.HandleFunc("/",showUploadfilePage)
	http.HandleFunc("/upload",uploadFileHandle)

	fmt.Println("服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/")

	// 啓動 HTTP 服務,並監聽端口號,開始監聽,處理請求,返回響應
	err := http.ListenAndServe(":8900", nil)
	if err != nil {
		log.Fatal("ListenAndServe",err)
	}
}
複製代碼

經過上面的代碼能夠看到,處理文件上傳須要調用 r.ParseMultipartForm,裏面的參數表示 maxMemory,調用 ParseMultipartForm 以後,上傳的文件存儲在 maxMemory 大小的內存裏面,若是文件大小超過了 maxMemory,那麼剩下的部分將存儲在系統的臨時文件中。能夠經過 r.FormFile 獲取上面的文件句柄,而後實例中使用了 io.Copy 來存儲文件。

獲取其它非文件字段信息的時候就不須要調用 r.ParseForm,由於在須要的時候 Go 自動會去調用。並且 ParseMultipartForm 調用一次以後,後面再次調用不會再有效果。

經過上面的實例可知上傳文件主要有三步處理:

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

文件 handler 是 multipart.FileHeader,裏面存儲了以下結構信息

type FileHeader struct {
    Filename string
    Header   textproto.MIMEHeader
    // contains filtered or unexported fields
}
複製代碼

執行以上程序後,在瀏覽器輸入 http://localhost:8900/ 訪問上傳文件頁面,服務器控制檯輸出:

服務器已經啓動,請在瀏覽器地址欄中輸入 http://localhost:8900/
複製代碼

而後點擊選擇須要上傳的文件:

點擊提交按鈕後,顯示上傳文件的信息以下:

最後,查看站點 upload 目錄,發現文件已經上傳完成

相關文章
相關標籤/搜索