go實踐二十 web開發--表單唯一token 表單驗證 防止xss攻擊 上傳文件 cookie處理

新建一個 testform2.gtpl 文件,內容如下:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>修改資料</title>
</head>
<body>
<form action="/info" method="post" enctype="multipart/form-data">
    頭像:<input type="file" name="userheader">
    <br>
    用戶名:<input type="text" name="username">
    <br>
    密碼:<input type="password" name="password">
    <br>
    年齡:<input type="text" name="age">
    <br>
    郵箱:<input type="text" name="email">
    <br>
    手機號碼:<input type="text" name="mobile">
    <br>
    身份證號碼:<input type="text" name="usercard">
    <br>
    測試xss:<input type="text" name="xss">
    <br>
    性別:
    <select name="sex" >
        <option value="0">無</option>
        <option value="1">男</option>
        <option value="2">女</option>
    </select>
    <br>
    興趣:
    <br>
    <input type="checkbox" name="interest" value="no">無
    <input type="checkbox" name="interest" value="football">足球
    <input type="checkbox" name="interest" value="basketball">籃球
    <input type="checkbox" name="interest" value="tennis">網球
    <br>
    <input type="hidden" name="token" value="{{.}}">
    <input type="submit" value="提交">
</form>
</body>
</html>

新建一個 testform2.go 文件,內容如下:

package main

import (
	"fmt"
	"crypto/md5"
	"html/template"
	texttemplate "text/template"
	"io"
	"os"
	"log"
	"time"
	"net/http"
	"strconv"
	"regexp"
	"unicode"
)



//判斷是否在切片中
func In_slice(val string, slice []string) bool {
	for _, v := range slice {
		if v == val {
			return true
		}
	}
	return false
}
//判斷兩個切片的差異
//返回差異的數組
func Slice_diff(slice1, slice2 []string) (diffslice []string) {
	for _, v := range slice1 {
		if !In_slice(v, slice2) {
			diffslice = append(diffslice, v)
		}
	}
	return
}


func sayhelloName(w http.ResponseWriter,r *http.Request)  {
	//下面這個寫入到w 的是輸出到客戶端的
	fmt.Fprintf(w,"hello testform2.go")
}

//驗證表單輸入
func info(w http.ResponseWriter,r *http.Request)  {
	fmt.Println("method:",r.Method) //獲取請求的方法
	if r.Method == "GET"{
		curtime := time.Now().Unix()
		h := md5.New()
		io.WriteString(h,strconv.FormatInt(curtime,10))
		token := fmt.Sprintf("%x",h.Sum(nil)) //生成token

		//設置cookie
		expiration := time.Now()
		expiration = expiration.AddDate(1, 0, 0)
		cookie := http.Cookie{Name: "username", Value: "go-cookie", Expires: expiration}
		http.SetCookie(w, &cookie)

		t,_ := template.ParseFiles("testform2.gtpl")
		t.Execute(w,token) //把token 寫入到模板中
	}else{
		//讀取cookie
		cookie, _ := r.Cookie("username")
		fmt.Println("cookie:",cookie)

		//防止多次重複提交表單
		//解決方案是在表單中添加一個帶有唯一值的隱藏字段。
		// 在驗證表單時,先檢查帶有該唯一值的表單是否已經遞交過了。
		// 如果是,拒絕再次遞交;如果不是,則處理表單進行邏輯處理。
		res1 := verifyToken(w,r)
		if !res1 {
			return
		}

		//表單驗證
		res2 := formValidate(w,r)
		if !res2 {
			return
		}

		//預防跨站腳本
		res3 := xss(w,r)
		if !res3 {
			return
		}

		//獲取上傳文件
		res4 := uploadHandler(w,r)
		if !res4{
			return
		}

	}

}
//防止多次重複提交表單
func verifyToken(w http.ResponseWriter,r *http.Request) bool{
	//r.ParseForm() //普通表單

	r.ParseMultipartForm(32<<20) //加入了 enctype="multipart/form-data" 的表單
	token := r.Form.Get("token")
	fmt.Println("token:",token)
	if token != ""{
		// 驗證 token 的合法性
		if len(token) <10{
			fmt.Fprintf(w,"token驗證失敗")
			return false
		}
		fmt.Fprintf(w,"token驗證通過")
	}else{
		//不存在token 報錯
		fmt.Fprintf(w,"token驗證失敗")
		return false
	}
	fmt.Fprintf(w,"\n")
	return true
}

//表單驗證
func formValidate(w http.ResponseWriter,r *http.Request) bool{
	//r.ParseForm()       //解析url傳遞的參數,對於POST則解析響應包的主體(request body)

	r.ParseMultipartForm(32<<20) //加入了 enctype="multipart/form-data" 的表單

	//請求的是登錄數據,name執行登錄的邏輯判斷
	fmt.Println("username:",r.Form["username"])
	fmt.Println("password:",r.Form["password"])
	fmt.Println("age:",r.Form["age"])
	fmt.Println("email:",r.Form["email"])
	fmt.Println("mobile:",r.Form["mobile"])
	fmt.Println("sex:",r.Form["sex"])
	fmt.Println("interest:",r.Form["interest"])
	fmt.Println("usercard:",r.Form["usercard"])

	//驗證表單數據--------------------------------------------------------
	//必填字段
	if len(r.Form["username"][0]) == 0{
		//返回到客戶端
		fmt.Fprintf(w,"用戶名不能爲空")
		return false
	}
	//unicode判斷中文
	for _, val := range r.Form.Get("username"){
		if unicode.Is(unicode.Scripts["Han"], val) {
			//有中文
			fmt.Fprintf(w,"unicode姓名不能有中文\n")
		}
	}
	//正則判斷中文
	chiReg := regexp.MustCompile("[\u4e00-\u9fa5]+")
	for _, val := range r.Form.Get("username") {
		if chiReg.MatchString(string(val)){
			//有中文
			fmt.Fprintf(w,"regexp姓名不能有中文\n")
			return false
		}
	}

	//判斷英文
	match2,_ := regexp.MatchString("^[a-zA-Z]+$",r.Form.Get("username"))
	if !match2{
		fmt.Fprintf(w,"姓名只能是英文字母")
		return false
	}


	//使用轉化判斷數字
	getint,err := strconv.Atoi(r.Form.Get("age"))
	if err != nil{
		//數字轉化出錯了,那麼可能就不是數字了
		fmt.Fprintf(w,"strconv年齡只能是數字\n")
	}
	//使用正則判斷數字 ,應該儘量避免使用正則表達式,因爲使用正則表達式的速度會比較慢
	m,_ := regexp.MatchString("^[0-9]+$",r.Form.Get("age"));
	if !m {
		fmt.Fprintf(w,"regexp年齡只能是數字\n")
		return false
	}

	//判斷數字的大小範圍
	if getint > 200{
		fmt.Fprintf(w,"年齡不能大於200")
		return false
	}


	//判斷電子郵件
	match3,_ := regexp.MatchString(`^([\w\.\_]{2,})@(\w{1,})\.([a-z]{2,4})$`,r.Form.Get("email"))
	if !match3{
		fmt.Fprintf(w,"電子郵箱格式不正確")
		return false
	}

	//判斷手機號碼
	match4,_ := regexp.MatchString(`^1[0-9][0-9]\d{8}$`,r.Form.Get("mobile"))
	if !match4{
		fmt.Fprintf(w,"手機號碼不正確")
		return false
	}

	//判斷身份證號碼
	//15位 或18位
	usercard := r.Form.Get("usercard")
	match5,_ := regexp.MatchString(`^(\d{15})$`,usercard)
	match6,_ := regexp.MatchString(`^(\d{17})([0-9]|X)$`,usercard)
	fmt.Println(usercard)
	fmt.Println(len(usercard))
	fmt.Println(!match6)
	if len(usercard) <= 15 && !match5{
		fmt.Fprintf(w,"身份證不正確")
		return false
	}else if len(usercard) > 15 && !match6{
		fmt.Fprintf(w,"身份證不正確")
		return false
	}

	//判斷下拉菜單的值是否符合預設值
	sex_slice := []string{"1","2"}
	sex_value := r.Form.Get("sex")
	is_set := 0 //是否符合預設值 ,1 是,0否
	for _,val := range sex_slice{
		if sex_value == val{
			is_set = 1
		}
	}
	if is_set == 0{
		fmt.Fprintf(w,"性別不正確")
		return false
	}

	//判斷複選框
	interest_slice := []string{"football","basketball","tennis"}
	interest := r.Form["interest"]
	array := Slice_diff(interest,interest_slice)
	fmt.Println("interest:",array)
	if array != nil{
		//有差異
		fmt.Fprintf(w,"興趣選擇不正確")
		return false
	}
	return true
}

//預防跨站腳本
func xss(w http.ResponseWriter,r *http.Request) bool{
	//template.HTMLEscape(w io.Writer, b []byte) //把b進行轉義之後寫到w
	//template.HTMLEscapeString(s string) string //轉義s之後返回結果字符串
	//template.HTMLEscaper(args ...interface{}) string //支持多個參數一起轉義,返回結果字符串

	//輸出到服務器端
	fmt.Println("xss:", template.HTMLEscapeString(r.Form.Get("xss")))

	//轉義後輸出到客戶端
	template.HTMLEscape(w, []byte(r.Form.Get("xss")))
	fmt.Fprintf(w, "\n") //換行

	//轉義後輸出到客戶端
	t,err := template.New("foo").Parse(`{{define "T"}}hello,{{.}}!{{end}}`)
	err = t.ExecuteTemplate(w,"T","<script>alert('you have been pwned')</script>")
	if err != nil{
		fmt.Println(err)
	}
	fmt.Fprintf(w, "\n") //換行

	//使用 text/template 完全輸出到客戶端
	t1,err1 := texttemplate.New("foo").Parse(`{{define "T"}}hello,{{.}}!{{end}}`)
	err1 = t1.ExecuteTemplate(w,"T","<script>alert('you have been pwned')</script>")
	if err1 != nil{
		fmt.Println(err1)
	}
	fmt.Fprintf(w, "\n") //換行

	//使用 html/template template.HTML 完全輸出到客戶端
	t2, err2 := template.New("foo").Parse(`{{define "T"}}hello,{{.}}!{{end}}`)
	err2 = t2.ExecuteTemplate(w,"T",template.HTML("<script>alert('you have been HTML')</script>"))
	if err2 != nil{
		fmt.Println(err2)
	}
	return true
}

//獲取上傳文件
func uploadHandler(w http.ResponseWriter,r *http.Request) bool{
	//通過上面的代碼可以看到,處理文件上傳我們需要調用r.ParseMultipartForm,
	// 裏面的參數表示maxMemory,調用ParseMultipartForm之後,
	// 上傳的文件存儲在maxMemory大小的內存裏面,
	// 如果文件大小超過了maxMemory,那麼剩下的部分將存儲在系統的臨時文件中。
	// 我們可以通過r.FormFile獲取上面的文件句柄,然後實例中使用了io.Copy來存儲文件。
	r.ParseMultipartForm(32<<20)
	file,handler,err := r.FormFile("userheader")
	//文件的handler 是multipart.FileHeader ,裏面存儲瞭如下結構信息
	/*
	type FileHeader struct{
		Filename string
		Header textproto.MIMEHeader
		//contains filtered or unexported fields
	}
	*/
	if err != nil{
		fmt.Println(err)
		return false
	}
	defer file.Close()
	fmt.Fprintf(w,"%v",handler.Header)
	f,err := os.OpenFile("./"+handler.Filename,os.O_WRONLY|os.O_CREATE,0777)
	if err != nil{
		fmt.Println(err)
		return false
	}
	defer f.Close()
	io.Copy(f,file)

	return true
}

func main() {
	http.HandleFunc("/",sayhelloName) //設置路由
	http.HandleFunc("/info",info) //設置路由
	err := http.ListenAndServe(":6665",nil) //設置監聽的端口
	if err != nil{
		log.Fatal("ListenAndServe:",err)
	}
}

運行 testform2.go ,啓動服務器 :

[root@izj6c4jirdug8kh3uo6rdez goweb]# go run testform2.go

瀏覽器運行 http://域名:6665/info  ,輸入數據後提交,客戶端顯示:

token驗證通過
&lt;script&gt;alert(123);&lt;/script&gt;
hello,&lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
hello,<script>alert('you have been pwned')</script>!
hello,<script>alert('you have been HTML')</script>!map[Content-Disposition:[form-data; name="userheader"; filename="a435c26724359a53730a52d919f106a4.png"] Content-Type:[image/png]]

服務器端即可看到表單參數:

method: POST
token: 11112bd3ffe4ba8ddaea713ef49f9806
username: [awef]
password: []
age: [3]
email: [[email protected]]
mobile: [12312345678]
sex: [1]
interest: [football]
usercard: [123456789123456123]
123456789123456123
18
false
interest: []
xss: &lt;script&gt;alert(123);&lt;/script&gt;

參考:https://www.golang123.com/book/9?chapterID=158

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章