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

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

import (
    "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

image-20211121090250497

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

image-20211121090518563

如果没有行 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。

服务器端的输出如下:

image-20211121091210553

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只会返回同名参数中的第一个,若参数不存在则返回空字符串。

运行结果:

image-20211121092630347

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 />
            <!--
                按钮:带事件,不带事件
                    事件:发生了某一件事
                    带事件:按钮被点击,会触发某一件事
                    不带事件:按钮被点击,页面没有反应。配合JavaScript

                button:无事件
                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 main

import (
    "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 main

import (
	"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)
	}
}

image-20211121104016744image-20211121104728295

输入信息后提交:

image-20211121104610472

image-20211121104803808

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 main

import (
    "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)
    }
}

运行结果也一样:

image-20211121114208977

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

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

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

仅替换一行代码即可:

image-20211121114645601

浏览器运行结果:

image-20211121114301989

(补充:自己的实现效果

image-20211121113846037

image-20211121114721988

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 main

import (
    "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 main

import (
    "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 string

    Header textproto.MIMEHeader

    // contains filtered or unexported fields

}

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

image-20211121115914055

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

image-20211121115955383

image-20211121120056419)

6、 客户端上传文件

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

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

package main

import (
    "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 方法写很多其他类似的字段。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ClimberCoding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值