前言:大家好,以下所有内容都是我学习韩茹老师的教程时所整理的笔记。部分内容有过删改, 推荐大家去看原作者的文档进行学习, 本文章仅作为个人的学习笔记,后续还会在此基础上不断修改。学习Go Web时应该已经熟悉Go语言基本语法以及计算机网络的相关内容。

学习链接:https://www.chaindesk.cn/witbook/17/253
参考书籍:《Go Web编程》谢孟军

第三章、表单操作

1、 处理表单的输入

先来看一个表单递交的例子,我们有如下的表单内容,命名成文件login.html(放入当前新建项目的目录里面)

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>登录页面</title></head><body><form action="http://127.0.0.1:8080/login" method="post">用户名:<input type="text" name="username"><br>密&nbsp&nbsp&nbsp码:<input type="password" name="password"><br><input type="submit" value="登陆"></form></body></html>

上面递交表单到服务器的/login,当用户输入信息点击登陆之后,会跳转到服务器的路由login里面,我们首先要判断这个是什么方式传递过来的,POST还是GET呢?

http包里面有一个很简单的方式就可以获取,我们在前面web的例子的基础上来看看怎么处理login页面的form数据,创建一个go文件:demo01_loginserver.go,代码如下:

package mainimport ("fmt""html/template""log""net/http""strings"
)func sayhello(w http.ResponseWriter, r *http.Request) {r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body)//注意:如果没有调用ParseForm方法,下面无法获取表单的数据fmt.Println(r.Form) //这些信息是输出到服务器端的打印信息fmt.Println("path: ", r.URL.Path)fmt.Println("scheme: ", r.URL.Scheme)fmt.Println(r.Form["url_long"])for k, v := range r.Form {fmt.Println("key: ", k)fmt.Println("val: ", strings.Join(v, ""))}fmt.Fprintf(w, "Hello my route!") //这个写入到w的是输出到客户端的
}
func login(w http.ResponseWriter, r *http.Request) {r.ParseForm() //解析url传递的参数,对于POST则解析响应包的主体(request body)//注意:如果没有调用ParseForm方法,下面无法获取表单的数据fmt.Println("method: ", r.Method) //获取请求的方法if r.Method == "GET" {t, _ := template.ParseFiles("login.html")t.Execute(w, nil)} else {//请求的是登陆数据,那么执行登陆的逻辑判断fmt.Println("username: ", r.Form["username"])fmt.Println("password: ", r.Form["password"])}
}
func main() {http.HandleFunc("/hello", sayhello)           //设置访问的路由http.HandleFunc("/login", login)         //设置访问的路由err := http.ListenAndServe(":8080", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}

然后打开浏览器,在地址栏输入:http://127.0.0.1:8080/login

输入用户名和密码后,点击按钮,观察服务器端的运行结果:

如果没有行 r.ParseForm()。我们输入用户名和密码之后发现在服务器端是不会打印出来任何输出的,为什么呢?默认情况下,Handler里面是不会自动解析form的,必须显式的调用 r.ParseForm() 后,你才能对这个表单数据进行操作。我们添加代码 r.ParseForm(),重新运行,再次测试输入递交,现在是不是在服务器端有输出我们的输入的用户名和密码了。

r.Form 里面包含了所有请求的参数,比如URL中 query-string、POST的数据、PUT的数据,所有当你在URL的 querystring 字段和POST冲突时,会保存成一个slice,里面存储了多个值,Go官方文档中说在接下来的版本里面将会把POST、GET这些数据分离开来。

现在我们修改一下login.html里面form的action值http://127.0.0.1:8080/login修改为

http://127.0.0.1:8080/login?username=ruby,再次测试,服务器的输出username就是一个slice。

服务器端的输出如下:

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"])

注意: Request本身也提供了 FormValue() 函数来获取用户提交的参数。如 r.Form[“username”] 也可写成 r.FormValue(“username”)。调用 r.FormValue 时会自动调用 r.ParseForm,所以不必提前调用。r.FormValue只会返回同名参数中的第一个,若参数不存在则返回空字符串。

运行结果:

2、 验证表单的输入

开发Web的一个原则就是,不能信任用户输入的任何信息,所以验证和过滤用户的输入信息就变得非常重要,我们经常会在微博、新闻中听到某某网站被入侵了,存在什么漏洞,这些大多是是因为网站对于用户输入的信息没有做严格的验证引起的,所以为了编写出安全可靠的Web程序,验证表单输入的意义重大。

我们平常编写Web应用主要有两方面的数据验证,一个是在页面端的js验证(目前在这方面有很多的插件库,比如ValidationJS插件),一个是在服务器端的验证,我们这小节讲解的是如何在服务器端验证。

2.1 必填字段

你想要确保从一个表单元素中得到一个值,例如前面小节里面的用户名,我们如何处理呢?Go有一个内置函数len可以获取字符串的长度,这样我们就可以通过len来获取数据的长度,例如:

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

r.Form 对不同类型的表单元素的留空有不同的处理, 对于空文本框、空文本区域以及文件上传,元素的值为空值,而如果是未选中的复选框和单选按钮,则根本不会在 r.Form 中产生相应条目,如果我们用上面例子中的方式去获取数据时程序就会报错。所以我们需要通过 r.Form.Get() 来获取值,因为如果字段不存在,通过该方式获取的是空值。

但是通过 r.Form.Get() 只能获取单个的值,如果是map的值,必须通过上面的方式来获取。

2.2 数字

你想要确保一个表单输入框中获取的只能是数字,例如,你想通过表单获取某个人的具体年龄是50岁还是10岁,而不是像“一把年纪了”或“年轻着呢”这种描述

如果我们是判断正整数,那么我们先转化成int类型,然后进行处理

getint, err := strconv.Atoi(r.Form.Get("age"))if err != nil {//数字转化出错了,那么可能就是不是数字
}//接下来就可以判断这个数字的大小范围了if getint >100 {//太大了
}

还有一种方式就是正则匹配的方式

if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m  {return false
}

对于性能要求很高的用户来说,这是一个老生常谈的问题了,他们认为应该尽量避免使用正则表达式,因为使用正则表达式的速度会比较慢。但是在目前机器性能那么强劲的情况下,对于这种简单的正则表达式效率和类型转换函数是没有什么差别的。如果你对正则表达式很熟悉,而且你在其它语言中也在使用它,那么在Go里面使用正则表达式将是一个便利的方式。

Go实现的正则是RE2,所有的字符都是UTF-8编码的。

2.3 中文

有时候我们想通过表单元素获取一个用户的中文名字,但是又为了保证获取的是正确的中文,我们需要进行验证,而不是用户随便的一些输入。对于中文我们目前有效的验证只有正则方式来验证,如下代码所示

if m, _ := regexp.MatchString(`^[\x{4e00}-\x{9fa5}]+$`, r.Form.Get("zhname")); !m {return false
}

2.4 英文

我们期望通过表单元素获取一个英文值,例如我们想知道一个用户的英文名,应该是rubyhan,而不是ruby韩。

我们可以很简单的通过正则验证数据:

if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("enname")); !m{return false
}

2.5 电子邮件地址

你想知道用户输入的一个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")
}

2.6 手机号码

你想要判断用户输入的手机号码是否正确,通过正则也可以验证:

if m, _ := regexp.MatchString(`^(1[3|5|6|7|8][0-9]\d{8})$`, r.Form.Get("mobile")); !m {return false
}

2.7 下拉菜单

如果我们想要判断表单里面<select>元素生成的下拉菜单中是否有被选中的项目。有些时候黑客可能会伪造这个下拉菜单不存在的值发送给你,那么如何判断这个值是否是我们预设的值呢?

我们的select可能是这样的一些元素:

学&nbsp&nbsp&nbsp历:
<!--
selected="selected"
-->
<select name="xueli"><option>--请选择--</option><option value="xiaoxue">小学</option><option value="chuzhong">初中</option><option value="gaozhong">高中</option><option value="dazhuan" >大专</option><option value="benke">本科</option><option value="shuoshi">硕士</option><option value="boshi">博士</option><option value="lieshi">烈士</option>
</select>

那么我们可以这样来验证:

/**
验证下拉列表*/
func checkSelect(xueli string) bool {slice := []string{"xiaoxue", "chuzhong", "gaozhong", "dazhuan", "benke", "shuoshi", "boshi", "lieshi"}for _, v := range slice {if v == xueli {return true}}return false
}

2.8 单选按钮

如果我们想要判断radio按钮是否有一个被选中了,我们页面的输出可能就是一个男、女性别的选择,但是也可能一个15岁大的无聊小孩,一手拿着http协议的书,另一只手通过telnet客户端向你的程序在发送请求呢,你设定的性别男值是1,女是2,他给你发送一个3,你的程序会出现异常吗?因此我们也需要像下拉菜单的判断方式类似,判断我们获取的值是我们预设的值,而不是额外的值。

<input type="radio" name="sex" value="male" checked="checked"/>男
<input type="radio" name="sex" value="female"/>女
<input type="radio" name="sex" value="other"/>其他

那我们也可以类似下拉菜单的做法一样:

/**
验证单选按钮*/
func checkSex(sex string) bool {slice := []string{"male", "female", "other"}for _, v := range slice {if v == sex {return true}}return false
}

2.9 复选框

有一项选择兴趣的复选框,你想确定用户选中的和你提供给用户选择的是同一个类型的数据。

爱&nbsp&nbsp&nbsp好:
<input type="checkbox" name="hobby" value="game" checked="checked"/>游戏
<input type="checkbox" name="hobby" value="girl" />女人
<input type="checkbox" name="hobby" value="money" />金钱
<input type="checkbox" name="hobby" value="power" />权利
<br />

对于复选框我们的验证和单选有点不一样,因为接收到的数据是一个slice:

/**
验证复选框*/
func checkHobby(hobby []string) bool {slice := []string{"game", "girl", "money", "power"}hobby2 := Slice_diff(hobby, slice)if hobby2 == nil {return true}return false
}func Slice_diff(slice1, slice2 []string) (diffslice []string) {for _, v := range slice1 {if !InSlice(v, slice2) {diffslice = append(diffslice, v)}}return
}/**
判断是一个切片中是否包含指定的数值*/
func InSlice(val string, slice []string) bool {for _, v := range slice {if v == val {return true}}return false
}

2.10 身份证号码

如果我们想验证表单输入的是否是身份证,通过正则也可以方便的验证,但是身份证现在都是18位,我们可以进行如下验证:

//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。
if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {return false
}

上面列出了我们一些常用的服务器端的表单元素验证,希望通过这个引导入门,能够让大家对Go的数据验证有所了解,特别是Go里面的正则处理。

2.11 完整代码

1.html页面:register.html

<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>验证表单</title></head><body><h1>注册信息</h1><form action="http://127.0.0.1:8080/register" method="post">用户名:<input type="text" name="username" id="username" /><br />密&nbsp&nbsp&nbsp码:<input type="password" name="pwd" id="pwd" /><br />中文名:<input type="text" name="zhname" id="zhname" /><br />英文名:<input type="text" name="enname" id="enname" /><br />年&nbsp&nbsp&nbsp龄:<input type="text" name="age" id="age" /><br />性&nbsp&nbsp&nbsp&nbsp别:<input type="radio" name="sex" value="male" checked="checked"/>男<input type="radio" name="sex" value="female"/>女<input type="radio" name="sex" value="other"/>其他<br />邮&nbsp&nbsp&nbsp箱:<input type="text" name="email" id="email" /><br />手机号码:<input type="text" name="mobile" id="mobile" /><br />身份证号:<input type="text" name="usercard" id="usercard"><br>爱&nbsp&nbsp&nbsp好:<input type="checkbox" name="hobby" value="game" checked="checked"/>游戏<input type="checkbox" name="hobby" value="girl" />女人<input type="checkbox" name="hobby" value="money" />金钱<input type="checkbox" name="hobby" value="power" />权利<br />学&nbsp&nbsp&nbsp历:<!--selected="selected"--><select name="xueli"><option>--请选择--</option><option value="xiaoxue">小学</option><option value="chuzhong">初中</option><option value="gaozhong">高中</option><option value="dazhuan" >大专</option><option value="benke">本科</option><option value="shuoshi">硕士</option><option value="boshi">博士</option><option value="lieshi">烈士</option></select><br />头&nbsp&nbsp&nbsp像:<input type="file" name="myfile" /><br />个人简介:<br /><textarea rows="8" cols="70"></textarea><br /><!--按钮:带事件,不带事件事件:发生了某一件事带事件:按钮被点击,会触发某一件事不带事件:按钮被点击,页面没有反应。配合JavaScriptbutton:无事件image:无事件reset:有事件,清空表单数据submit:有事件,提交表单当submit按钮被点击,触发form表单中action属性的路径(服务器地址)提交方式:get:默认词意:获取,获得url?username=zhangsan&pwd=123456&sex=female..url:请求的路径地址?前是请求路径后本次请求提交的数据传递的数据:采用名值对的形式参数名=参数值&参数名=参数值。。。不安全:数据暴露了传递少量的数据容易乱码post:词意:邮政邮局数据打包之后,传递给服务端数据安全可以传递大量的数据不容易乱码--><input type="button" value="按钮" /><input type="reset" value="重置"/><input type="image" src="img/qq.gif" /><input type="submit" value="提交" /></form></body>
</html>

2.go文件:demo02_checkform.go

package mainimport ("fmt""log""net/http""strconv""regexp"
)func register(w http.ResponseWriter, r *http.Request) {r.ParseForm()//1.验证必填字段//username := r.Form["username"][0]username := r.Form.Get("username")if len(username) == 0 {fmt.Println("用户名不能为空!")fmt.Fprintf(w, "用户名不能为空!") //这个写入到w的是输出到客户端的}//2.验证数字age, err := strconv.Atoi(r.Form.Get("age"))if err != nil {//数字转化出错了,那么可能就是不是数字fmt.Println("您输入的不是数字!")fmt.Fprintf(w, "您输入的不是数字!") //这个写入到w的是输出到客户端的}//接下来就可以判断这个数字的大小范围了if age > 100 || age < 0 {//太大了或太小了fmt.Println("您输入的年龄太大了或太小了,请输入0-100之间的整数!")fmt.Fprintf(w, "您输入的年龄太大了或太小了,请输入0-100之间的整数!") //这个写入到w的是输出到客户端的}//或者正则表达式if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {fmt.Println("验证有误,您输入的年龄太大了或太小了!")fmt.Fprintf(w, "验证有误,您输入的年龄太大了或太小了!")}//3.验证中文if m, _ := regexp.MatchString(`^[\x{4e00}-\x{9fa5}]+$`, r.Form.Get("zhname")); !m {fmt.Println("验证有误,请输入中文!")fmt.Fprintf(w, "验证有误,请输入中文!")}//4. 验证英文if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("enname")); !m {fmt.Println("验证有误,请输入英文!")fmt.Fprintf(w, "验证有误,请输入英文!")}//5. 邮箱if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {fmt.Println("请输入正确邮箱地址")fmt.Fprintf(w, "验证有误,请输入正确邮箱地址!")}//6. 验证手机号if m, _ := regexp.MatchString(`^(1[3|5|6|7|8][0-9]\d{8})$`, r.Form.Get("mobile")); !m {fmt.Println("请输入正确手机号码")fmt.Fprintf(w, "验证有误,请输入正确手机号码!")}//7. 下拉菜单xueli := r.Form.Get("xueli")res1 := checkSelect(xueli)if !res1 {fmt.Println("请选择正确的下拉列表!")fmt.Fprintf(w, "请选择正确的下拉列表!")}// 8. 单选按钮sex := r.Form.Get("sex")res2 := checkSex(sex)if !res2 {fmt.Println("请选择正确的性别!")fmt.Fprintf(w, "请选择正确的性别!")}// 9. 复选框hobby := r.Form["hobby"]res3 := checkHobby(hobby)if !res3 {fmt.Println("请选择正确的爱好!")fmt.Fprintf(w, "请选择正确的爱好!")}// 10 身份证号//验证18位身份证,18位前17位为数字,最后一位是校验位,可能为数字或字符X。if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {fmt.Println("请选择正确的身份证号!")fmt.Fprintf(w, "请选择正确的身份证号!")}//fmt.Println("验证成功!")//fmt.Fprintf(w, "验证成功!")}/**
验证下拉列表*/
func checkSelect(xueli string) bool {slice := []string{"xiaoxue", "chuzhong", "gaozhong", "dazhuan", "benke", "shuoshi", "boshi", "lieshi"}for _, v := range slice {if v == xueli {return true}}return false
}/**
验证单选按钮*/
func checkSex(sex string) bool {slice := []string{"male", "female", "other"}for _, v := range slice {if v == sex {return true}}return false
}/**
验证复选框*/
func checkHobby(hobby []string) bool {slice := []string{"game", "girl", "money", "power"}hobby2 := Slice_diff(hobby, slice)if hobby2 == nil {return true}return false
}func Slice_diff(slice1, slice2 []string) (diffslice []string) {for _, v := range slice1 {if !InSlice(v, slice2) {diffslice = append(diffslice, v)}}return
}/**
判断一个切片中是否包含指定的数值*/
func InSlice(val string, slice []string) bool {for _, v := range slice {if v == val {return true}}return false
}func main() {http.HandleFunc("/register", register)   //设置访问的路由err := http.ListenAndServe(":8080", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}

(补充:下面是自己实现的一表单:

package mainimport ("fmt""log""net/http""regexp""strconv""text/template"
)func register(w http.ResponseWriter, r *http.Request) {r.ParseForm()fmt.Println("method: ", r.Method) // 获取请求的方法if r.Method == "GET" {t, _ := template.ParseFiles("register.html")t.Execute(w, nil)} else {// 1. 验证必填字段username := r.Form.Get("username")if len(username) == 0 {fmt.Println("用户名不能为空!")fmt.Fprintf(w, "用户名不能为空!")}// 2. 验证数字age, err := strconv.Atoi(r.Form.Get("age"))if err != nil {// 数字转换出错,那么可能就不是数字了fmt.Println("您输入的不是数字!")fmt.Fprintf(w, "您输入的不是数字!")}// 接下来判断数字的大小范围if age > 100 || age < 0 {fmt.Println("您输入的年龄太大或太小,请输入0-100之间的整数")fmt.Fprintf(w, "您输入的年龄太大或太小,请输入0-100之间的整数")}// 或者正则表达式if m, _ := regexp.MatchString("^[0-9]+$", r.Form.Get("age")); !m {fmt.Println("您输入的年龄太大或太小,请输入0-100之间的整数")fmt.Fprintf(w, "您输入的年龄太大或太小,请输入0-100之间的整数")}// 3. 验证中文if m, _ := regexp.MatchString(`^[\x{4e00}-\x{9fa5}]+$`, r.Form.Get("zhname")); !m {fmt.Println("验证有误,请输入中文")fmt.Println(w, "验证有误,请输入中文")}// 4. 验证英文if m, _ := regexp.MatchString("^[a-zA-Z]+$", r.Form.Get("enname")); !m {fmt.Println("验证有误,请输入英文")fmt.Println(w, "验证有误,请输入英文")}// 5. 邮箱if m, _ := regexp.MatchString(`^([\w\.\_]{2,10})@(\w{1,}).([a-z]{2,4})$`, r.Form.Get("email")); !m {fmt.Println("验证有误,请输入正确邮箱地址")fmt.Println(w, "验证有误,请输入正确邮箱地址")}// 6. 验证手机号if m, _ := regexp.MatchString(`^(1[3|5|6|7|8][0-9]\d{8})$`, r.Form.Get("mobile")); !m {fmt.Println("请输入正确手机号码")fmt.Fprintf(w, "验证有误,请输入正确手机号码!")}// 7. 下拉菜单xueli := r.Form.Get("xueli")res1 := checkSelect(xueli)if !res1 {fmt.Println("请选择正确的下拉列表!")fmt.Fprintf(w, "请选择正确的下拉列表!")}// 8. 单选按钮sex := r.Form.Get("sex")res2 := checkSex(sex)if !res2 {fmt.Println("请选择正确的性别!")fmt.Fprintf(w, "请选择正确的性别!")}// 9. 复选框hobby := r.Form["hobby"]res3 := checkHobby(hobby)if !res3 {fmt.Println("请选择正确的爱好!")fmt.Fprintf(w, "请选择正确的爱好!")}// 10. 身份证号if m, _ := regexp.MatchString(`^(\d{17})([0-9]|X)$`, r.Form.Get("usercard")); !m {fmt.Println("请选择正确的身份证号!")fmt.Fprintf(w, "请选择正确的身份证号!")}// 请求的是登录数据,那么执行登录的逻辑判断fmt.Println("用户名: ", r.Form["username"])fmt.Println("密码: ", r.Form["pwd"])fmt.Println("中文名: ", r.Form["zhname"])fmt.Println("英文名: ", r.Form["enname"])fmt.Println("年龄: ", r.Form["age"])fmt.Println("性别: ", r.Form["sex"])fmt.Println("邮箱: ", r.Form["email"])fmt.Println("电话: ", r.Form["mobile"])fmt.Println("身份证号: ", r.Form["usercard"])fmt.Println("爱好: ", r.Form["hobby"])fmt.Println("学历: ", r.Form["xueli"])fmt.Println("头像: ", r.Form["myfile"])}}// 验证下拉列表
func checkSelect(xueli string) bool {slice := []string{"xiaoxue", "chuzhong", "gaozhong", "dazhuan", "benke", "shuoshi", "boshi", "lieshi"}for _, v := range slice {if v == xueli {return true}}return false
}// 验证单选按钮
func checkSex(sex string) bool {slice := []string{"male", "female", "other"}for _, v := range slice {if v == sex {return true}}return false
}// 验证复选框
func checkHobby(hobby []string) bool {slice := []string{"game", "girl", "money", "power"}hobby2 := Slice_diff(hobby, slice)return hobby2 == nil
}func Slice_diff(slice1, slice2 []string) (diffslice []string) {for _, v := range slice1 {if !InSlice(v, slice2) {diffslice = append(diffslice, v)}}return
}// 判断是一个切片中是否包含指定的数值
func InSlice(val string, slice []string) bool {for _, v := range slice {if v == val {return true}}return false
}func main() {http.HandleFunc("/register", register)   // 设置访问的路由err := http.ListenAndServe(":8080", nil) // 设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}

输入信息后提交:

3、 预防跨站脚本

现在的网站包含大量的动态内容以提高用户体验,比过去要复杂得多。所谓动态内容,就是根据用户环境和需要,Web应用程序能够输出相应的内容。动态站点会受到一种名为 “跨站脚本攻击”(Cross Site Scripting, 安全专家们通常将其缩写成 XSS)的威胁,而静态站点则完全不受其影响。

攻击者通常会在有漏洞的程序中插入JavaScript、VBScript、 ActiveX或Flash以欺骗用户。一旦得手,他们可以盗取用户帐户信息,修改用户设置,盗取/污染cookie和植入恶意广告等。

对XSS最佳的防护应该结合以下两种方法:一是验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);另一个是对所有输出数据进行适当的处理,以防止任何已成功注入的脚本在浏览器端运行。

那么Go里面是怎么做这个有效防护的呢?Go的 html/template 里面带有下面几个函数可以帮你转义

  • func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w
  • func HTMLEscapeString(s string) string //转义s之后返回结果字符串
  • func HTMLEscaper(args …interface{}) string //支持多个参数一起转义,返回结果字符串

创建go文件,demo03_template.go,代码如下:

(补充:下面这段代码自己实现效果与文档不同,暂时未找到原因。)

package mainimport ("fmt""html/template""log""net/http"
)func login(w http.ResponseWriter, r *http.Request) {r.ParseForm()username := r.Form.Get("username")fmt.Println("username:", template.HTMLEscapeString(username)) //输出到服务器端fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))template.HTMLEscape(w, []byte(username)) //输出到客户端//fmt.Fprintf(w, username) //这个写入到w的是输出到客户端的(补充:下面更改的应是这里的username)
}func main() {http.HandleFunc("/login", login)      //设置访问的路由err := http.ListenAndServe(":8080", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}

如果我们输入的username是<script>alert()</script>,那么我们可以在浏览器上面看到输出如下所示:

或者是:

func login2(w http.ResponseWriter, r *http.Request){r.ParseForm()username := r.Form.Get("username")fmt.Println(username)//进行模板解析t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)err = t.ExecuteTemplate(w, "T", username)//如果转义失败 抛出对应错误 终止程序if err != nil {log.Fatal(err)}
}

运行结果也一样:

Go的html/template包默认帮你过滤了html标签,但是有时候你只想要输出这个<script>alert()</script>看起

来正常的信息,该怎么处理?请使用template.HTML类型。

err = t.ExecuteTemplate(w, "T", template.HTML(username))

仅替换一行代码即可:

浏览器运行结果:

(补充:自己的实现效果

4、 防止多次递交表单

不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢?

解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。

创建一个html的模板文件test.gtpl,添加代码如下:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>防止多次提交</title>
</head>
<body><form action="http://127.0.0.1:8080/login" method="post">用户名:<input type="text" name="username"><br>密&nbsp&nbsp&nbsp码:<input type="password" name="password"><br><input type="hidden" name="token" value="{{.}}"><input type="submit" value="登陆"></form>
</body>
</html>

创建一个go文件,demo04_server.go,代码如下:

package mainimport ("fmt""html/template""log""net/http""time""crypto/md5""strconv""io""os"
)func login(w http.ResponseWriter, r *http.Request) {fmt.Println("method:", r.Method) //获取请求的方法if r.Method == "GET" {crutime := time.Now().Unix()h := md5.New()io.WriteString(h, strconv.FormatInt(crutime, 10))token := fmt.Sprintf("%x", h.Sum(nil))fmt.Println("token--->", token)t, _ := template.ParseFiles("test.gtpl")t.Execute(w, token)} else {//请求的是登陆数据,那么执行登陆的逻辑判断r.ParseForm()token := r.Form.Get("token")if token != "" {//验证token的合法性fmt.Println("token:", token)} else {//不存在token报错fmt.Println("token有误。。")}fmt.Println("username length:", len(r.Form["username"][0]))fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password")))template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端}
}func main() {http.HandleFunc("/login", login)         //设置访问的路由err := http.ListenAndServe(":8080", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}

我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。

我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。

5、 处理文件上传

你想处理一个由用户上传的文件,比如你正在建设一个类似Instagram的网站,你需要存储用户拍摄的照片。这种需求该如何实现呢?

要使表单能够上传文件,首先第一步就是要添加form的enctype属性,enctype属性有如下三种情况:

application/x-www-form-urlencoded 表示在发送前编码所有字符(默认)
multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。
text/plain 空格转换为 "+" 加号,但不对特殊字符编码。

新建html页面(upload.html),html页面代码:

<!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:8080/upload" method="post"><input type="file" name="uploadfile"/><br><input type="hidden" name="token" value="{{.}}"/><br><input type="submit" value="upload"/></form>
</body>
</html>

新建go文件(demo05_uploadserver.go),go文件代码:

package mainimport ("fmt""html/template""log""net/http""time""crypto/md5""strconv""io""os"
)func main() {http.HandleFunc("/upload", upload)err := http.ListenAndServe(":8080", nil) //设置监听的端口if err != nil {log.Fatal("ListenAndServe: ", err)}
}// 处理/upload 逻辑
func upload(w http.ResponseWriter, r *http.Request) {fmt.Println("method:", r.Method) //获取请求的方法if r.Method == "GET" {crutime := time.Now().Unix()h := md5.New()io.WriteString(h, strconv.FormatInt(crutime, 10))token := fmt.Sprintf("%x", h.Sum(nil))t, _ := template.ParseFiles("upload.gtpl")t.Execute(w, token)} else {r.ParseMultipartForm(32 << 20)file, handler, err := r.FormFile("uploadfile")if err != nil {fmt.Println(err)return}defer file.Close()fmt.Fprintf(w, "%v", handler.Header)f, err := os.OpenFile("./test/"+handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)if err != nil {fmt.Println(err)return}defer f.Close()io.Copy(f, file)}
}

通过上面的代码可以看到,处理文件上传我们需要调用 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 stringHeader textproto.MIMEHeader// contains filtered or unexported fields}

我们通过上面的实例代码打印出来上传文件的信息如下:

(补充:自己实现的直接报错了。。

)

6、 客户端上传文件

我们上面的例子演示了如何通过表单上传文件,然后在服务器端处理文件,其实Go支持模拟客户端表单功能支持文件上传,详细用法请看如下示例:

创建一个go文件,来表示客户端:demo06_uploadclient.go,代码如下:

package mainimport ("bytes""fmt""io""io/ioutil""mime/multipart""net/http""os"
)func postFile(filename string, targetUrl string) error {bodyBuf := &bytes.Buffer{}bodyWriter := multipart.NewWriter(bodyBuf)//关键的一步操作fileWriter, err := bodyWriter.CreateFormFile("uploadfile", filename)if err != nil {fmt.Println("error writing to buffer")return err}//打开文件句柄操作fh, err := os.Open(filename)if err != nil {fmt.Println("error opening file")return err}//iocopy_, err = io.Copy(fileWriter, fh)if err != nil {return err}contentType := bodyWriter.FormDataContentType()bodyWriter.Close()resp, err := http.Post(targetUrl, contentType, bodyBuf)if err != nil {return err}defer resp.Body.Close()resp_body, err := ioutil.ReadAll(resp.Body)if err != nil {return err}fmt.Println(resp.Status)fmt.Println(string(resp_body))return nil
}// sample usage
func main() {target_url := "http://localhost:8080/upload"filename := "./正则验证.docx"postFile(filename, target_url)
}

以上代码详细展示了客户端如何向服务器上传一个文件,客户端通过 multipart.Write 把文件的文本流写入一个缓存中,然后调用http的Post方法把缓存传到服务器。

如果你还有其他普通字段例如 username 之类的需要同时写入,那么可以调用 multipart 的 WriteField 方法写很多其他类似的字段。

【Go Web学习笔记】第三章 Go与表单的操作相关推荐

  1. 《Go语言圣经》学习笔记 第三章 基础数据类型

    <Go语言圣经>学习笔记 第三章 基础数据类型 目录 整型 浮点数 复数 布尔型 字符串 常量 注:学习<Go语言圣经>笔记,PDF点击下载,建议看书. Go语言小白学习笔记, ...

  2. 机器人导论(第四版)学习笔记——第三章

    机器人导论(第四版)学习笔记--第三章 3 操作臂运动学 3.1 引言 3.2 连杆的描述 3.3 连杆连接的描述 3.4 连杆坐标系的定义 3.5 操作臂运动学 3.6 驱动空间.关节空间和笛卡尔空 ...

  3. HiveQL学习笔记(三):Hive表连接

    本系列是本人对Hive的学习进行一个整理,主要包括以下内容: 1.HiveQL学习笔记(一):Hive安装及Hadoop,Hive原理简介 2.HiveQL学习笔记(二):Hive基础语法与常用函数 ...

  4. 【Go Web学习笔记】第二章 HTTP的客户端实现

    前言:大家好,以下所有内容都是我学习韩茹老师的教程时所整理的笔记.部分内容有过删改, 推荐大家去看原作者的文档进行学习, 本文章仅作为个人的学习笔记,后续还会在此基础上不断修改.学习Go Web时应该 ...

  5. 【Go Web学习笔记】第一章 搭建Web服务器

    前言:大家好,以下所有内容都是我学习韩茹老师的教程时所整理的笔记.部分内容有过删改, 推荐大家去看原作者的文档进行学习, 本文章仅作为个人的学习笔记,后续还会在此基础上不断修改.学习Go Web时应该 ...

  6. 2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第三章:简单控件

    第 3 章 简单控件 本章介绍了App开发常见的几类简单控件的用法,主要包括:显示文字的文本视图.容纳视图的常用布局.响应点击的按钮控件.显示图片的图像视图等.然后结合本章所学的知识,演示了一个实战项 ...

  7. Win32学习笔记 第三章 HelloWin 选择自 villager 的 Blog

    Win32学习笔记 作者: 姜学哲(netsail0@163.net) 教材: Windows程序设计(第五版)北京大学出版社  [美]Charles Petzold 著  北京博彦科技发展有限公司 ...

  8. ros2 Navigation 学习笔记 第三章(the construct 网站)

    第三章 如何在环境中给机器人定位 序言 本章包括: 机器人定位的意义(即需要知道它对于环境中的位置和方向) 如何在ROS2中用AMCL定位 如何设置机器人的初始位置(手动.自动与使用ROS API) ...

  9. 信号与系统学习笔记 第三章

    第三章 周期信号的傅里叶级数表示 下面将讨论信号与线性时不变系统的另一种表示,讨论的出发点仍是将信号表示成一组基本信号的线性组合.这是因为,将信号表示成基本信号的线性组合是有利的,如果基本信号具有一下 ...

  10. 仿生学导论学习笔记——第三章

    第三章 仿生学基本要素 3.1 仿生需求 3.1.1 需求驱动 3.1.1.1 生存需求 3.1.1.2 健康需求 3.1.1.3 军事需求 3.1.1.4 发展需求 3.1.1.5 精神需求 3.1 ...

最新文章

  1. Scrapy-Splash的介绍、安装以及实例
  2. roobo机器人怎么唱歌_日本“观音”机器人问世,可以陪僧人念佛经
  3. 解决WORDPRESS评论时头像不显示的问题
  4. r roc函数_画ROC曲线的R包总结
  5. 【计算机网络】计算机网络概述
  6. 7-118 估值一亿的AI核心代码 (20 分)
  7. python3清屏命令_python3.4中清屏的处理方法
  8. Android学习路线指南
  9. gif分解合成_如何通过分解和合成使复杂的问题更容易
  10. 2000款学校教师课件培训PPT模板免费下载网址
  11. 网络安全-点击劫持(ClickJacking)的原理、攻击及防御
  12. 深度学习图像-半自动和自动标注工具
  13. Cocos Creator 微信小游戏排行榜
  14. 通过贝叶斯非参数模型探索在物流风险评估中的大数据
  15. 运维岗位面试被问到的问题
  16. 【有利可图网】PS干货:学会这个LOGO设计思路,分分钟拿下甲方!
  17. 安科瑞电气火灾监控系统对分散在建筑内的探测器进行遥测、遥调、遥控、遥信,方便实现监控与管理。
  18. strcmpi,stricmp函数
  19. 响应式网页设计教程:展示响应式设计的基本原理
  20. python顺序结构例题_python的顺序结构、选择结构、循环结构的练习代码

热门文章

  1. Apache架设代理服务器
  2. 程序员啊 ,请保护好你的眼睛 !
  3. 前端图片上传问题整理
  4. Frodo and pillows
  5. 在Mac下载安装win10虚拟机教程及分辨率等问题。
  6. 微信刷票python代码_微信刷票漏洞详解, Python脚本实现一秒破万!
  7. 时间序列分析|LSTM多变量时间序列预测
  8. 硬件工程师要学习的东西
  9. 终端应用变身文件 MD5/SHA1 校验工具
  10. 鼠标设置按键功能方式(例如设置鼠标侧键为复制粘贴功能)