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 的时候,出现如下界面

界面图.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表单的时候都是唯一的,用户递交的表单保持了唯一性。

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编程》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,061评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,407评论 3 404
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,275评论 0 368
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,084评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,091评论 6 400
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,555评论 1 315
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,914评论 3 429
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,900评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,438评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,470评论 3 346
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,596评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,187评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,932评论 3 340
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,361评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,511评论 1 277
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,163评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,671评论 2 366

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,750评论 18 139
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    阳明先生_X自主阅读 15,992评论 3 119
  • 青岛又一次下雪了。 这次算得上是 纷纷扬扬吧 作为一个南方人 每次看到雪都没有什么特别的反应 倒是总会想起朋友的一...
    抱抱兔子写写文_阅读 201评论 0 0
  • Alfred3中支持了Snippets,这样大大方便了一些懒人,然而想用它却不那么容易,这不就遇到坑了。 一开始是...
    千若逸阅读 4,968评论 24 0
  • 1、和领导请示汇报的工作如果领导说和另外一个领导沟通,那你一定要尽快跟进,不要超过一周时间。2、涉及到股份总部和集...
    独慎阅读 452评论 0 0