【译】发送表单数据

这是原文链接:sending form data

许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是了解一些细节还是很重要的,以避免损坏您的服务器或者给您的用户带来麻烦。

发送数据到哪?

客户端/服务器架构

web是基于非常简单的客户端/服务器架构的,可以总结为一下两点:

  1. 客户端(通常是浏览器)通过HTTP协议发送请求给服务器(大部分服务器就是Apache,Nginx,IIS,Tomcat)
  2. 服务器同样使用HTTP协议来返回响应

在客户端,没有比表单更方便而且用户友好的方式来配置HTTP请求去发送数据到服务器端了。这使得用户可以通过HTTP请求来传递信息。

在客户端:定义怎样发送数据

表单元素定义了怎样发送数据。其所有属性就是被设计用来配置HTTP请求的。最主要的两个属性是action和method。

关于action属性
该属性定义了数据被发送到哪。其值必须是合法的URL。如果没有提供该属性,那么数据发送到当前页面。

例子

这个例子中,数据被发送到http://foo.com

<form action="http://foo.com">

这个例子中,数据被发送到相同的服务器中,但是与当前页面不同的URL

<form action="/somewhere_else">

如果没有指定该属性,那么数据被发送到当前页面

<form>

许多以前的网页中使用下面的代码来指明数据应该被发送到当前页面,应为在HTML5之前,该属性是必须的,现在已经没有必要了这样写了

<form action="#">

注意:可以指定URL使用https协议。如果这样做了,那么即使表单本身处于不安全的网页中(使用http协议),表单数据也会被加密。另一方面,如果表单本身处于安全网页中,但是指定URL使用不安全的http协议,那么每次当用户提交数据时,浏览器都会显示不安全的警告给用户,因为数据没有被加密。

关于method属性

该属性定义了数据怎样被发送。HTTP协议定义了许多种request;表单数据至少可以通过两种方法来发送:GET和POST。

为了理解两种发送方法的不同,我们先回顾一下HTTP工作原理。每次当你想要获取一个网络上的资源,浏览器会发送一个请求。这个请求包含两个部分:header和body。其中header包含了一系列的关于浏览器能力的全局元数据,body则是包含一些发送到服务器端的信息(只用部分发送方法的请求有body部分,有些请求没有body部分)。

关于GET方法
GET方法说明浏览器想要服务器端返回一个资源:“嘿,服务器,给我返回这个资源”。这种请款下,浏览器发送的请求没有body部分。因为如果表单采用这种方法发送数据,数据会被添加到URL后面,所以body是空的。

例子

考虑下面的表单:

<form action="http://foo.com" method="get">
  <input name="say" value="Hi">
  <input name="to" value="Mom">
  <button>Send my greetings</button>
</form>

如果使用GET方法,那么请求应该是这样的:

GET /?say=Hi&to=Mom HTTP/1.1
Host: foo.com

关于POST方法
POST方法有些不同。浏览器希望服务器处理这些数据:“嘿,服务器,看看这些数据然后给我一个结果”。发送的数据处在请求的body部分。

例子

考虑下面的表单(和上面的一样)

<form action="http://foo.com" method="get">
  <input name="say" value="Hi">
  <input name="to" value="Mom">
  <button>Send my greetings</button>
</form>

现在使用POST方法,那么请求应该是这样的:

POST / HTTP/1.1
Host: foo.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13

say=Hi&to=Mom

Content-Length字段指明body的大小,Content-Type字段指明发送到服务器端的资源类型(其实就是编码方式)。

当然,HTTP协议从来不会显示给用户(不过可以通过浏览器的开发者工具来查看)。唯一显示给用户的就是URL。所以在GET方法中,用户可以从网址栏中看见数据,但是在POST方法中,就看不见了。这一点非常重要:

  1. 如果将要发送用户名和密码(或者其他敏感信息),一定不能使用GET方法,否则就会在地址栏显示出来。
  2. 如果将要发送巨量数据,最好使用POST方法,应为某些浏览器会限制URL的长度,并且许多服务器也会限制可以接收的URL长度。

在服务器端:解析并获取数据

不管采用哪种请求方法,服务器端会接收到一个字符串,然后解析该字符串,得到一系列的键值对。获取这些数据的方式取决于你选择的开发平台和特定开发框架。不同的技术同样影响到重复键的处理方法;一般情况下,后接受到的优先级更大,会覆盖掉之前的数据。

例子:原生PHP

PHP提供了一些全局对象去获取这些数据。假设我们使用POST方法提交数据,下面的例子仅仅是获取这些数据然后展示给用户。当然,怎样处理这些数据取决于你。你可以显示数据,存储数据,发送邮件或者其他方式。

<?php
  // The global $_POST variable allows you to access the data sent with the POST method
  // To access the data sent with the GET method, you can use $_GET
  $say = htmlspecialchars($_POST['say']);
  $to  = htmlspecialchars($_POST['to']);

  echo  $say, ' ', $to;

上面例子的结果是:

Hi Mom

例子:原生Python

本例子使用Python实现相同的功能--显示网页提供的数据。例子中使用CGI Python package来获取数据。

#!/usr/bin/env python
import html
import cgi
import cgitb; cgitb.enable()     # for troubleshooting

print("Content-Type: text/html") # HTTP header to say HTML is following
print()                          # blank line, end of headers

form = cgi.FieldStorage()
say  = html.escape(form["say"].value);
to   = html.escape(form["to"].value);

print(say, " ", to)

显示结果和上面的例子一样:

Hi Mom

其他语言和框架

还有许多其他的服务端技术可以处理表单,包括Perl,Java,.Net,Ruby等等。选择你最喜欢的就好了。但是也没有必要直接使用这些技术去处理表单,因为会比较繁琐。更通常的做法是选择一种框架来辅助处理表单,比如:

  • Symfony for PHP
  • Django for Python
  • Ruby On Rails for Ruby
  • Grails for Java

虽然使用这些框架来处理表单不一定就是非常容易,但是总是好一些,并且可以节省大量时间。

一种特殊情况:发送文件

发送文件对表单来说是一种特殊情况。文件是二进制数据--至少被当作二进制数据--但是其他数据都是文本数据。因为HTTP协议是一种文本协议,所以处理二进制数据是特殊需求了。

enctype属性

该属性可以指定Content-Type字段的值。该字段非常重要,因为可以让服务器端识别出发送的数据类型(编码类型)。默认值是application/x-www-urlencoded。对人类而言,这意味着:“表单数据已经被编码成URL形式了”。

如果我们想要发送文件,应该需要做两件事:

  1. 设置method属性为POST,因为文件内容不能放到URL中。
  2. 设置enctype属性为multipart/form-data,这样文件会被分割成很多部分发送到服务器。

举个例子:

<form method="post" enctype="multipart/form-data">
  <input type="file" name="myFile">
  <button>Send the file</button>
</form>

注意:有些浏览器的input元素支持multiple属性,从而支持一个input元素可以发送多个文件。服务器端怎样处理这些文件完全依赖与服务器端选择的技术。前面也提到过,使用框架来处理这些东西会稍微轻松些。

警告:许多服务器都限制了上传文件的大小和HTTP请求的大小,以防止滥用资源。和服务器管理员确定这个限制的大小是非常重要的。

安全问题

当你每次发送数据给服务器时,你都应该考虑安全问题。HTML表单是服务器的主要威胁之一。不过问题并不是来源于表单本身,而是服务器如何处理表单数据。

常见安全漏洞

以下是一些众所周知的安全问题:

XSS和CSRF

跨站点脚本攻击(XSS)和跨站点请求伪造(CSRF)是常见的攻击类型,这通常发生在服务器接收用户的数据然后再次显示这些数据给用户。

XSS使得攻击者可以注入客户端脚本到网页中。一个XSS漏洞可能会让攻击者可以绕过访问控制,比如同源策略。这些攻击的影响可大可小,可以是比较小的干扰,也可以是重大安全风险。

CSRF和XSS有些相似--因为他们都需要注入客户端脚本到网页中--但是CSRF的目的又不一样。CSRF攻击者会提升自己的特权(比如网站管理员),然后就可以做一些本来没有权利的操作,比如发送数据给不信任的用户。

XSS攻击利用用户对网站的信任,CSRF攻击利用网站对用户的信任。

为避免这些攻击,你应该检查用户发送到服务器的所有数据,而且不应该显示用户提供的HTML内容。相反,你应该处理这些数据而不是一字不变的显示这些数据。如今大部分框架都实现了基本的HTML字符过滤,比如过滤掉<script>,<iframe>,<object>。这可以降低风险,但还是不能完全避免掉。

SQL注入

SQL注入是一种尝试操作网站数据库的一种攻击。攻击者通常会发送SQL请求并期望服务器去执行它(其实就是存储数据),这也是服务器的主要威胁之一

这种攻击的后果是非常严重的,轻一点就是数据丢失,严重的就是特权提升到可以操作所有服务器资源。这种威胁是非常严峻的,所以你永远不应该直接存储用户提交的数据,而是需要做一些检查以及消毒工作(比如在PHP/MySQL应用中使用mysql_real_escape_string())。

HTTP header注入 以及 email 注入

如果你使用表单数据来建立HTTP header或者email,那么就有可能发生这些攻击。他们不会直接攻击服务器或者影响到用户,但却是更深层次问题的后门,比如会话劫持或者钓鱼攻击。

这些攻击大部分都是静悄悄的,但是却会将你的服务器变为肉鸡

警记:永远不要相信你的用户

那么怎样处理这些威胁呢?这些内容已经超出本章内容了,但是仍然有一些规则需要牢记在心中。最最重要的规则就是:永远不要相信用户,包括自己;因为一个可信任的用户也可能被劫持。

所有到达服务器的数据都需要检查和消毒,总是这样做,不要存在例外。

  • 转义潜在的危险的字符。根据数据内容的不同以及部署平台的不同,需要小心的字符也有所区别。但是所有服务器端语言都有相关的函数来处理这种转义。
  • 限制数据的大小以及必须的类型。
  • 上传文件到沙箱(存储这些文件到一个不同的服务器,而且只允许通过一个不同的子域名来访问文件,甚至最好是完全不同的域名来访问文件)。

如果你遵守上面三条规则,你应该可以避免掉绝大部分的难题了,但是邀请第三方做一个安全审查仍然是一个好主意。永远不要假设你已经解决了所有问题。

总结

如你所见,发送表单数据是非常容易的,但是只做安全的web应用则是复杂的。要记住作为前端开发者不仅仅只是定义数据模型。我们还要做客户端的数据校验,但是服务器端仍然不能信任这些校验结果,因为服务器可没办法知道客户端的真实情况。

另请参见

下面两个链接是关于安全web应用方面的,可以继续参考学习:

  1. The Open Web Application Security Project (OWASP)
  2. Chris Shiflett's blog about PHP Security

下面是关于http方面的链接

  1. GET/POST之enctype
  2. GET vs. POST
  3. What's the difference between “Request Payload” vs “Form Data” as seen in Chrome dev tools Network tab
  4. How are parameters sent in an HTTP POST request?

个人补充

上面更多的内容是关于web安全方面的,下面补充一点method以及enctype方面的信息。

表单中method属性用来指定发送数据的方法:比如GET/POST/PUT等等
表单中enctype属性用来指定发送的数据的编码方式:比如text/plain,application/json,application/x-www-form-urlencoded,application/octet-stream,multipart/form-data等等

GET/POST/PUT最大的不同当然是语义不一样,不过这里只研究对于发送数据的影响。
GET发送的数据位于URL中的query string部分,而URL又处于请求header部分。
POST/PUT发送的数据位置倒是一样,都是位于请求body部分。

不同的method会影响到发送数据的位置,而enctype则会影响到数据的编码方式。
form元素的enctype默认值则是application/x-www-form-urlencoded,所以发送的数据可能长得像这样:

username=name123&password=pass456&age=12&sex=1

注意这种编码形式和GET/POST/PUT是没有关系的。只不过如果是GET方法,那么URL加上问号(?)再加上数据拼接起来。如果是POST/PUT方法,那么数据就存放在请求body部分。

对于jquery中的ajax方法的contentType默认值也是application/x-www-form-urlencoded。
而在backbone中,Backbone.sync方法中有一段代码会做出判断,可能会将contentType设置为application/json。

先说说multipart/form-data,一般在上传文件的时候,需要将表单的enctype设置为multipart/form-data。正如上面译文中所讲,文件属于二进制数据,application/x-www-form-urlencoded是不适用的。
不过这种编码方式过于复杂,不像上面的urlencoded编码,只需要两次分割字符串就能得到所有的键值对。对于这种复杂的编码方式,我们自己去解析数据,是很难的,幸好已经有开源的库完成了这一部分功能。
对于单文件上传也许还好,自己了解编码方式以后,解析起来也不是很难。难点在于多文件上传,甚至还有普通表单控件混在其中。

再说说application/json,据我理解urlencoded编码方式只适用于扁平化的键值对,对于嵌套过深的json对象就很难编码了。而application/json就非常适用于嵌套过深的json数据。
在chrome的开发者工具中的network页签中,显示发送数据位于Request Payload。而如果是application/x-www-form-urlencoded,就会显示发送数据位于Form Data。

实际上是可以自定义contentType的,也就是自定义编解码的规则。不过只针对ajax有效,对于form元素是不起作用的。因为form元素的编码规则有浏览器控制。而对于ajax,我们完全可以自己编码,然后在服务器端自己解码。

最后一点,如果服务器端不能理解Content-Type指定的编码方式,那么应该返回415错误。

你可能感兴趣的:(数据)