0x00 前言

初学 Web 编程的时候发现想通过 HTML 表单上传文件的时候发现只需要简单的设置 form 标签的 enctype 属性为 multipart/form-data 便能成功的实现,后面才知道其实这是在设置表单内容的编码并且设置 HTTP 请求头的 Content-Type 属性。而在 W3C 标准里要求浏览器必须支持 application/x-www-form-urlencodedmultipart/form-data 这两种 enctype 方式,下面就来解释一下这两种类型吧!

0x01 application/x-www-form-urlencoded

注意:application/x-www-form-urlencoded 类型无法用于具有文件上传功能的表单。

浏览器 form 标签的 enctype 属性默认即为 application/x-www-form-urlencoded,从字面上看我们看到了一个 urlencoded。猜测是不是和 URL 相关?没错其实这种方式就是把我们平常用的 GET 方法所提交的参数放在了 HTTP 请求的 body 里,下面是一个通过 URL 传参例子:

1
http://127.0.0.1:8000/?keyword=panda&author=zane

可以看见重点在于 ? 号后面的部分,这部分指定了传递的参数的键和值。然后我们看一下通过 Postman 指定 Content-Typeapplication/x-www-form-urlencoded 所发送的 POST 请求报文:

1
2
3
4
5
6
7
POST  HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: e8c31d7a-fd52-cbf2-2741-07d73cb1101b

keyword=panda&author=zane

通过比较,发现请求体中的内容和URL? 后的内容相等。

注意:和 URL 编码一致,当内容出现非 ASCII 字符时会被编码为 %HH 的形式(H是十六进制数的意思⊙o⊙)。

现在我们来点更详细的,将前面 URL 对应的 GET 请求报文贴出来:

1
2
3
4
GET /?keyword=panda&author=zane HTTP/1.1
Host: 127.0.0.1:8000
Cache-Control: no-cache
Postman-Token: 43550a7c-2579-cea1-5dd5-55cbfac077f8

可以看见 HTTP 请求的 body 部分为空,所传递的参数在第一行(请求行)的 URL 里。所以当有人问你 HTTPGETPOST 的区别的时候,你就可以告诉他「GET 将请求数据放在 URL 里, POST 将请求数据放在请求体里」。

偏题内容:为何说 GET 方法能提交数据要比 POST 少?

我在了解了 TCP 协议后便觉得其实 GETPOST 并无区别,因为对于 TCP 协议来说都只是数据而已。站在传输层的角度来说 URL 应该可以无限长,所以使用 GET 能提交数据不应该比 POST 少。但事实真的如此吗?稍微作个实验就知道是错的,因为这个限制出自「浏览器」和「 Web 服务器」。从浏览器的角度考虑,允许输入一个无限长文本的地址栏很明显就是一个错误的设计。此外在服务器的角度来说,因为 URLHTTP 报文的第一行,如果允许 URL 无限长就意味着要等待 URL 全部接收完毕才可以获取其它头信息和请求体,对于一些需要验证某些头字段后才允许访问页面来说无疑带来了极大的资源的浪费(比如说接收到了一个很长的 URL,但是后面的请求头的字段内容错误或者干脆就是不符合 HTTP 格式,这样前面接收 URL 所耗费的资源不都统统浪费了吗? ),这样不限制 URL 长度便容易被人所以利用攻击。因此大多数的「浏览器」和「 Web 服务器」都有限制 URL 长度,所以说在提交表单 POST 还是比 GET 好滴O(∩_∩)O!如果想了解更多的内容可以在 StackOverflow 上看这个问题 What is the maximum length of a URL in different browsers?,本人技术浅薄不敢多言(ㄒoㄒ)。

0x02 multipart/form-data

multipart/form-dataapplication/x-www-form-urlencoded 要复杂,和上面提交同样的数据,报文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
Cache-Control: no-cache
Postman-Token: 090df52c-d103-279b-1479-50e6a7fef58b

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="keyword"

panda
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="author"

zane
------WebKitFormBoundary7MA4YWxkTrZu0gW--

首先我们看请求头的 Content-Type 它除了正常的 multipart/form-data 外还多了一个 boundary ,这个 boundary 的意思和字面意思一样就是分界线,通过分界线将每个键值对用 boundary 分割开来以示区别。现在我们看请求体,我们注意到boundary 将键值对分割后的每一部分都有 Content-Disposition 字段,实际上该字段的值必须为 form-data 而且后面必须加上 name 指定这部分的键名,然后是一行空行,空行之后便是提交数据的内容。 之所以要弄的这么复杂是因为 multipart/form-data 要支持文件上传。下面是一个包含文件上传的示例报文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST / HTTP/1.1
Host: 127.0.0.1:8000
Cache-Control: no-cache
Postman-Token: 8c62dfea-a64e-aeda-5f5a-d40557edfb18
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="keyword"

panda
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="author"

zane
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="logo.png"
Content-Type: image/png

// 文件内容
------WebKitFormBoundary7MA4YWxkTrZu0gW--

得益于分界线使每部分内容都可以分开,所以文件的内容可以直接以二进制传输而不用经过编码。另外上传文件应该通过 filename 指定文件名并且通过 Content-Type 指定文件的 MIME 类型。

PHP 关于 multipart/form-data 的坑

注意 PHP 默认只解析 POST 方法的 multipart/form-data 请求(就是只有发送 POST 请求时才能获取到 multipart/form-data 提交的内容),也就说如果用其它的方法如 PUTPATCHDELETE 发送 multipart/form-data 类型的请求,PHP 默认不会解析所以获取不到内容(当然大神可以自己通过 php://input 解析内容),但 application/x-www-form-urlencoded 是都可以用的。遇到这个坑是因为在使用 Postman 调试 API 时发现请求的参数怎么都获取不到,结果发现 Postman 选了 multipart/form-data 类型,然后请求方法是 PUT。原来代码一切正常,却调了半天(ㄒoㄒ)……

0x03 总结

最后附上两条终极链接 application/x-www-form-urlencoded or multipart/form-data? HTML 表单之必知必会(手动狗头),O(∩_∩)O哈哈~~如果能看完就肯定会觉得我写的很垃圾了(ㄒoㄒ)。文章有误欢迎指出,欢迎友好讨论!