diff --git a/4.3.md b/4.3.md index cb705646..783e889f 100644 --- a/4.3.md +++ b/4.3.md @@ -6,11 +6,12 @@ 对XSS最佳的防护应该结合以下两种方法:验证所有输入数据,有效检测攻击(这个我们前面小节已经有过介绍);对所有输出数据进行适当的编码,以防止任何已成功注入的脚本在浏览器端运行。 -那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转移 +那么Go里面是怎么做这个有效防护的呢?Go的html/template里面带有下面几个函数可以帮你转义 + +- func HTMLEscape(w io.Writer, b []byte) //把b进行转义之后写到w +- func HTMLEscapeString(s string) string //转义s之后返回结果字符串 +- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,返回结果字符串 -- func HTMLEscape(w io.Writer, b []byte) //把需要转移的b进行转义之后写到w -- func HTMLEscapeString(s string) string //转移s之后返回转义的字符串 -- func HTMLEscaper(args ...interface{}) string //支持多个参数一起转义,然后返回一个字符串 我们看4.1小节的例子 @@ -18,11 +19,11 @@ fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 -我们看到最后输出到客户端的代码如下: +如果我们输入的username是``,那么我们可以在浏览器上面看到输出如下所示: ![](images/4.3.escape.png?raw=true) -那么我们在输出我们的模板的时候怎么处理的呢?Go的html/template包现在默认就帮你过滤了html元素,但是有时候你又想输出这样的信息,text/template也支持。请看下面的例子所示 +那么我们在输出我们的模板的时候怎么处理的呢?Go的html/template包默认帮你过滤了html元素,但是有时候你又想输出这样的信息,请使用text/template。请看下面的例子所示 import "text/template" ... diff --git a/4.4.md b/4.4.md index f947d8cf..8de992b8 100644 --- a/4.4.md +++ b/4.4.md @@ -1,9 +1,56 @@ -#4.4防止多次递交表单 - -## links - * [目录]() - * 上一节: [预防跨站脚本](<4.3.md>) - * 下一节: [处理文件上传](<4.5.md>) - -## LastModified +#4.4防止多次递交表单 + +不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? + +解决方案是在表单中添加一个带有唯一值得隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 + +我继续拿4.2小节的例子优化: + + 用户名: + 密码: + + + +我们看到在模版里面我们增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存) + + 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)) + + t, _ := template.ParseFiles("login.gtpl") + t.Execute(w, token) + } else { + //请求的是登陆数据,那么执行登陆的逻辑判断 + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + //验证token的合法性 + } else { + //不存在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"))) //输出到客户端 + } + } + +上面的代码输出到页面的源码如下: + +![](images/4.4.token.png?raw=true) + +我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 + +我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 + +## links + * [目录]() + * 上一节: [预防跨站脚本](<4.3.md>) + * 下一节: [处理文件上传](<4.5.md>) + +## LastModified * $Id$ \ No newline at end of file diff --git a/4.5.md b/4.5.md new file mode 100644 index 00000000..888eec77 --- /dev/null +++ b/4.5.md @@ -0,0 +1,87 @@ +#4.5处理文件上传 +你想处理一个由用户上传的文件。例如,你正在建设一个类似Instagram 的网站,所以需要处理和存储用户提供的照片。我们该如何处理呢? + +文件要能够上传,首先第一步就是要修改form的`enctype`属性,`enctype`属性有如下三种情况: + + application/x-www-form-urlencoded 表示在发送前编码所有字符(默认) + multipart/form-data 不对字符编码。在使用包含文件上传控件的表单时,必须使用该值。 + text/plain 空格转换为 "+" 加号,但不对特殊字符编码。 + +我们如果要使得文件能够上传,那么我们的html应该如下所示 + + + + 上传文件 + + +
+ + + +
+ + + +我们增加一个handlerFunc如下: + + http.HandleFunc("/upload", upload) + + // 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("file") + if err != nil { + fmt.Println(err) + } + 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 { + panic(err) + } + defer f.Close() + io.Copy(f, file) + } + } + +通过上面的代码我们可以看到,文件上传我们需要调用`r.ParseMultipartForm`,里面的参数表示maxMemory,调用`ParseMultipartForm`之后我们上传的文件存储在设置的那么大的内存里面,如果文件大小超过了这个内存,那么剩下的部分存储在系统的临时文件中。我们可以通过`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 + } + +我们通过上面的实例代码打印出来上传文件的信息如下 + +![](images/4.5.upload2.png?raw=true) + + + +## links + * [目录]() + * 上一节: [防止多次递交表单](<4.4.md>) + * 下一节: [小结](<4.6.md>) + +## LastModified + * $Id$ \ No newline at end of file diff --git a/4.6.md b/4.6.md new file mode 100644 index 00000000..4c562cd3 --- /dev/null +++ b/4.6.md @@ -0,0 +1,12 @@ +#4.6 小结 +这一章里面我们学习了Go里面如何处理表单信息,我们通过一个登陆、一个上传例子展示了Go处理form表单信息,处理文件上传的能力。但是在处理表单过程中我们需要验证用户输入的信息,考虑到网站安全、数据过滤就变得相当重要了,因此专门一个小节讲解了各方面的数据过滤,顺带讲了一下Go里面对正则的处理。 + +通过这一章能够让你了解客户端和服务器端如何进行数据的交互,让客户端的数据进入我们的系服务器统,让我们系统处理之后的数据展现给客户端。 + +## links + * [目录]() + * 上一节: [处理文件上传](<4.4.md>) + * 下一章: [访问数据库](<5.md>) + +## LastModified + * $Id$ \ No newline at end of file diff --git a/images/4.4.token.png b/images/4.4.token.png new file mode 100644 index 00000000..b52cc1d1 Binary files /dev/null and b/images/4.4.token.png differ diff --git a/images/4.5.upload.png b/images/4.5.upload.png new file mode 100644 index 00000000..e5766e38 Binary files /dev/null and b/images/4.5.upload.png differ diff --git a/images/4.5.upload2.png b/images/4.5.upload2.png new file mode 100644 index 00000000..06460446 Binary files /dev/null and b/images/4.5.upload2.png differ