Go语言 Web开发(4)表单

在WEB应用编程时,最常用到的是form表单工具。表单是一个包含表单元素的区域。表单元素是允许用户在表单中(比如:文本域、下拉列表、单选框、复选框等等)输入信息的元素。表单使用表单标签(<form>)定义。比如:

<form>
...
input  元素
...
</form>

我们现在来演示一个使用Go语言做Web表单登录的例子
(1)处理表单的输入
我们现在在新建项目的目录里创建一个html,写入以下代码

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

上面表单代码的意思是我们输入用户名和密码点击登录后,客户端就会提交表单到服务器的/login下。

下面是登录后端的代码逻辑

package main

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

func main() {
   //指定访问的路由
   http.HandleFunc("/login",login)
   //设定监听端口
   err := http.ListenAndServe(":9090",nil)
   if err != nil {
      log.Fatal("ListenAndServe: ",err)
   }
}

/**
   登录后端处理逻辑
*/
func login(rw http.ResponseWriter,request *http.Request){
   // 获取请求的方法 request.Method 字符串类型的变量,返回GET, POST,PUT等method信息。
   fmt.Printf("method: %v\n",request.Method)
   //根据 request.Method 来判断是显示登录界面还是处理登录逻辑
   if request.Method == "GET" {
      //使用template.ParseFiles 根据指定的文件创建模板示例
      t,_ := template.ParseFiles("login.html")
      //执行数据融合
      t.Execute(rw,nil)
   }else {
      //请求的是登录数据,那么执行登录的逻辑判断
      //解析传过来的参数,默认不会解析,必须显示调用后服务器才会输出参数信息
      request.ParseForm()
      //这里的request.Form["username"]可以用request.FormValue("username")代替,那么就不需要显示调用  request.ParseForm
      fmt.Printf("username: %v\n",request.Form["username"]) 
      fmt.Printf("password: %v\n",request.Form["password"])
   }
}

当我们在浏览器里面打开 http://127.0.0.1:9090/login 的时候,出现如下界面

《Go语言 Web开发(4)表单》 界面图.png

第一次登录网址时是显示登录界面,此时 request.Method 是 GET,后台机会使用template.ParseFiles 根据指定的文件创建模板示例(浏览器上的视图显示就是这么来的)。等我们输入用户名与密码再登录后 request.Method 就会成为POST。如果我们需要在服务器上输出传输过来的数据,就必须显示的调用 request.ParseForm(),默认情况下是不会显示调用的。

request.Form 是一个url.Values类型,里面存储的是对应的类似 key=value(key与value都是string类型) 的信息。上面的request.Form[“username”]可以用request.FormValue(“username”)代替,那么就不需要显示调用 request.ParseForm了,因为调用 request.FormValue 时会自动调用 request.ParseForm。但是如果同一个键中包含多个值,使用 request.FormValue 时只会返回第一个值,有关request.FormValue 的源码如下:

/**
FormValue返回查询的命名组件的第一个值。

POST和PUT正文参数优先于URL查询字符串值。

如有必要,FormValue会调用ParseMultipartForm和ParseForm,并忽略这些函数返回的任何错误。

如果key不存在,FormValue将返回空字符串。

要访问同一个键的多个值,请调用ParseForm,然后直接检查Request.Form。
*/
func (r *Request) FormValue(key string) string {
   if r.Form == nil {
      r.ParseMultipartForm(defaultMaxMemory)
   }
   if vs := r.Form[key]; len(vs) > 0 {
      return vs[0] //返回第一值
   }
   return ""
}

(2)验证表单的输入
在不使用正则表达式的情况下,如何判断以下情况:
①判断传输过来的字符串为空字符串或为空(无法判断字符串中间是否为空)

//使用 request.FormValue 获取传输的数据
username := request.FormValue("username")
password := request.FormValue("password")
//使用len(strings.TrimSpace(username)) 判断用户名与密码是否为空字符串
fmt.Printf("username: %v, len(strings.TrimSpace(username))= %v\n",username, len(strings.TrimSpace(username)))
fmt.Printf("password: %v,len(strings.TrimSpace(username))= %v\n", password,len(strings.TrimSpace(password)))

升级版:将字符串所有的空格消除后判断字符串是否为空字符串

//使用 strings.Join + strings.Fields + strings.TrimSpace 
username := strings.Join(strings.Fields(strings.TrimSpace(request.FormValue("username"))),"")
password := strings.Join(strings.Fields(strings.TrimSpace(request.FormValue("password"))),"")
fmt.Println("username: ",username," password: ",password)

当然也可以使用下面这些方法

strings.Replace(username," ","",-1) //注意 第一个为“ ”中间有个空格,后面的“”是没有空格的,-1代表将前面的字符全部替换后面的字符

(3)判断传输过来的数据都是数字

//先经过去空字符串操作处理,再使用strconv.Atoi
getint1,err1:=strconv.Atoi(username)
getint2,err2:=strconv.Atoi(password)
fmt.Println("username: ",username," password: ",password)
if  err1 != nil || err2 != nil{
   fmt.Println("数字无法转换:",username,password)
}else if  getint1 > 100 || getint2 > 100{
   fmt.Println("数字太大:",username,password)
}else {
   fmt.Println("数字:",username,password)
}

(4)过滤标签,预防脚本攻击
为了防止用户在客户端上进行脚本攻击(比如在客户端上插入JavaScript、VBScript、 ActiveX或Flash等方式欺骗服务器),我们可以使用Go的html/template包会自动过滤html标签,自动进行数据转义。比如这三个函数:

//以下三个是返回纯文本数据的转义HTML等价物。
func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w

func HTMLEscapeString(s string) string //转义s之后返回结果字符串

func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串

比如我们在用户名上输入<script>alert()</script>,在调用上面的函数后,就会自动进行转义操作

username := strings.Join(strings.Fields(strings.TrimSpace(request.FormValue("username"))),"")
password := strings.Join(strings.Fields(strings.TrimSpace(request.FormValue("password"))),"")
fmt.Println("username:", template.HTMLEscapeString(username)) // 输出到服务器端
fmt.Println("password:", template.HTMLEscapeString(password))
template.HTMLEscape(rw, []byte(username)) // 输出到客户端

客户端会输出以下结果:

&lt;script&gt;alert()&lt;/script&gt;

(5)防止多次递交表单
为了防止用户在同一个表单多次提交,我们可以是在模版里面增加了一个隐藏字段 token ,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到
服务器端,以方便表单提交时比对判定。

客户端模板上添加隐藏字段token

<input type="hidden" name="token" value="{{.}}">

后端的代码处理逻辑

if request.Method == "GET" {
   crutime := time.Now().Unix() //获取经过的秒数
   h := md5.New() //获取新的hash值
   io.WriteString(h,strconv.FormatInt(crutime,10)) //写入io
   token := fmt.Sprintf("%x",h.Sum(nil)) //格式化
   //使用template.ParseFiles 根据指定的文件创建模板示例
   t, _ := template.ParseFiles("login.html")
   //执行数据融合
   t.Execute(rw, token)
}

按照上面的代码执行后,在客户端的源代码界面上的token值就会由唯一的初始值,且会根据刷新实时更新,这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。

《Go语言 Web开发(4)表单》 Imagewdx.png

(6)处理文件上传

在客户端进行文件上传操作,我们需要在表单中添加form的 enctype 属性, enctype 属性有如下三种情况:

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

根据 enctype 属性,修改的客户端代码如下所示:

<!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="upload" />
</form>
</body>
</html>

服务器端上传文件代码如下所示:

func upload(rw http.ResponseWriter, request *http.Request) {
   //获取请求方式:post还是get
   fmt.Println("method: ", request.Method)
   if request.Method == "GET" {
      //获取经过的秒数
      cruTime := time.Now().Unix()
      //获取新的hash值,防止页面重复提交
      h := md5.New()
      //将cruTime进行10进制转换,并写入io
      io.WriteString(h, strconv.FormatInt(cruTime, 10))
      //Sum将当前哈希附加到b并返回结果切片。
      token := fmt.Sprintf("%x", h.Sum(nil))
      //使用template.ParseFiles 根据指定的文件创建模板示例
      t, _ := template.ParseFiles("login.html")
      //执行数据融合展现到客户端页面
      t.Execute(rw, token)
   } else {
      //解析整个请求体,并将其文件部分的总maxMemory字节存储在内存中,其余部分存储在临时文件的磁盘上。
      request.ParseMultipartForm(32 << 20)
      //通过 request.FormFile 获取客户端的文件句柄
      file, handler, err := request.FormFile("uploadfile")
      if err != nil {
         fmt.Println(err)
         return
      }
      defer file.Close()
      fmt.Fprintf(rw, "%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 来存储文件
      io.Copy(f, file)
   }
}

注意
①ParseMultipartForm(maxMemory int64) 这个函数,调用之后,上传的文件存储在 maxMemory 大小的内存里面,如果文件大小超过了maxMemory ,那么剩下的部分将存储在系统的临时文件中。

②调用request.FormFile(“uploadfile”)函数,获取文件句柄,然后对文件进行存储等处理。

参考书籍:《Go Web编程》

    原文作者:小杰的快乐时光
    原文地址: https://www.jianshu.com/p/0e70a4ede741
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞