2007年01月25日

  Rich Internet Application 是 Web 2.0 中的新时髦词,并且就 Web 2.0 的实质而言,一个关键组件就是 Adobe Flash。了解如何将 Flash 动画集成到应用程序中,并使用 Ming 库动态生成 Flash 动画。

  本文中提供的使用 Flash 动画的第一种方法是使用 Ming 库动态生成它们。Ming 库是一个 PHP 库,其中有一组映射到 SWF 动画中的数据类型的对象:子图形、图形、文本、位图等等。我将不讨论如何构建和安装 Ming,因为其操作是特定于平台的而且并不特别简单。在本文中,我使用了预编译的扩展 php_ming.dll 库用于 Windows 版本的 PHP。

全文登载于IBM developerWorks,地址:http://www-128.ibm.com/developerworks/cn/opensource/os-php-flash/index.html

2006年08月04日

大家都知道安全性是重要的,但是行业中的趋势是直到最后一刻才添加安全性。既然不可能完全保护 Web 应用程序,那么为什么要费这个劲儿呢,不是吗?不对。只需采用一些简单的步骤就能够大大提高 PHP Web 应用程序的安全性。

开始之前

在本教程中,您将学习如何在自己的 PHP Web 应用程序中添加安全性。本教程假设您至少有一年编写 PHP Web 应用程序的经验,所以这里不涉及 PHP 语言的基本知识(约定或语法)。目标是使您了解应该如何保护自己构建的 Web 应用程序。

目标

本教程讲解如何防御最常见的安全威胁:SQL 注入、操纵 GET 和 POST 变量、缓冲区溢出攻击、跨站点脚本攻击、浏览器内的数据操纵和远程表单提交。

前提条件

本教程是为至少有一年编程经验的 PHP 开发人员编写的。您应该了解 PHP 的语法和约定;这里不解释这些内容。有使用其他语言(比如 Ruby、Python 和 Perl)的经验的开发人员也能够从本教程中受益,因为这里讨论的许多规则也适用于其他语言和环境。

系统需求

需要一个正在运行 PHP V4 或 V5 和 MySQL 的环境。可以使用 Linux、OS X 或 Microsoft Windows。如果是在 Windows 上,那么下载 WAMPServer 二进制文件,在机器上安装 Apache、MySQL 和 PHP。

安全性快速简介

Web 应用程序最重要的部分是什么?根据回答问题的人不同,对这个问题的答案可能是五花八门。业务人员需要可靠性和可伸缩性。IT 支持团队需要健壮的可维护的代码。最终用户需要漂亮的用户界面和执行任务时的高性能。但是,如果回答 “安全性”,那么每个人都会同意这对 Web 应用程序很重要。

但是,大多数讨论到此就打住了。尽管安全性在项目的检查表中,但是往往到了项目交付之前才开始考虑解决安全性问题。采用这种方式的 Web 应用程序项目的数量多得惊人。开发人员工作几个月,只在最后才添加安全特性,从而让 Web 应用程序能够向公众开放。

结果往往是一片混乱,甚至需要返工,因为代码已经经过检验、单元测试并集成为更大的框架,之后才在其中添加安全特性。添加安全性之后,主要组件可能会停止工作。安全性的集成使得原本顺畅(但不安全)的过程增加额外负担或步骤。

本教程提供一种将安全性集成到 PHP Web 应用程序中的好方法。它讨论几个一般性安全主题,然后深入讨论主要的安全漏洞以及如何堵住它们。在学完本教程之后,您会对安全性有更好的理解。

主题包括:

  • SQL 注入攻击
  • 操纵 GET 字符串
  • 缓冲区溢出攻击
  • 跨站点脚本攻击(XSS)
  • 浏览器内的数据操纵
  • 远程表单提交

Web 安全性 101

在讨论实现安全性的细节之前,最好从比较高的角度讨论 Web 应用程序安全性。本节介绍安全哲学的一些基本信条,无论正在创建何种 Web 应用程序,都应该牢记这些信条。这些思想的一部分来自 Chris Shiflett(他关于 PHP 安全性的书是无价的宝库),一些来自 Simson Garfinkel(参见 参考资料),还有一些来自多年积累的知识。

规则 1:绝不要信任外部数据或输入

关于 Web 应用程序安全性,必须认识到的第一件事是不应该信任外部数据。外部数据(outside data) 包括不是由程序员在 PHP 代码中直接输入的任何数据。在采取措施确保安全之前,来自任何其他来源(比如 GET 变量、表单 POST、数据库、配置文件、会话变量或 cookie)的任何数据都是不可信任的。

例如,下面的数据元素可以被认为是安全的,因为它们是在 PHP 中设置的。

清单 1. 安全无暇的代码

<?php
$myUsername 
'tmyer';
$arrayUsers = array('tmyer''tom''tommy');
define("GREETING"'hello there' $myUsername);
?>

但是,下面的数据元素都是有瑕疵的。

清单 2. 不安全、有瑕疵的代码

<?php
$myUsername 
$_POST['username']; //tainted!
$arrayUsers = array($myUsername'tom''tommy'); //tainted!
define("GREETING"'hello there' $myUsername); //tainted!
?>

为什么第一个变量 $myUsername 是有瑕疵的?因为它直接来自表单 POST。用户可以在这个输入域中输入任何字符串,包括用来清除文件或运行以前上传的文件的恶意命令。您可能会问,“难道不能使用只接受字母 A-Z 的客户端(JavaScript)表单检验脚本来避免这种危险吗?”是的,这总是一个有好处的步骤,但是正如在后面会看到的,任何人都可以将任何表单下载到自己的机器上,修改它,然后重新提交他们需要的任何内容。

解决方案很简单:必须对 $_POST['username'] 运行清理代码。如果不这么做,那么在使用 $myUsername 的任何其他时候(比如在数组或常量中),就可能污染这些对象。

对用户输入进行清理的一个简单方法是,使用正则表达式来处理它。在这个示例中,只希望接受字母。将字符串限制为特定数量的字符,或者要求所有字母都是小写的,这可能也是个好主意。

清单 3. 使用户输入变得安全

<?php
$myUsername 
cleanInput($_POST['username']); //clean!
$arrayUsers = array($myUsername'tom''tommy'); //clean!
define("GREETING"'hello there' $myUsername); //clean!

function cleanInput($input){
    
$clean strtolower($input);
    
$clean preg_replace("/[^a-z]/"""$clean);
    
$clean substr($clean,0,12);
    return 
$clean;
}
?>

规则 2:禁用那些使安全性难以实施的 PHP 设置

已经知道了不能信任用户输入,还应该知道不应该信任机器上配置 PHP 的方式。例如,要确保禁用 register_globals。如果启用了 register_globals,就可能做一些粗心的事情,比如使用 $variable 替换同名的 GET 或 POST 字符串。通过禁用这个设置,PHP 强迫您在正确的名称空间中引用正确的变量。要使用来自表单 POST 的变量,应该引用 $_POST['variable']。这样就不会将这个特定变量误会成 cookie、会话或 GET 变量。

要检查的第二个设置是错误报告级别。在开发期间,希望获得尽可能多的错误报告,但是在交付项目时,希望将错误记录到日志文件中,而不是显示在屏幕上。为什么呢?因为恶意的黑客会使用错误报告信息(比如 SQL 错误)来猜测应用程序正在做什么。这种侦察可以帮助黑客突破应用程序。为了堵住这个漏洞,需要编辑 php.ini 文件,为 error_log 条目提供合适的目的地,并将 display_errors 设置为 Off。

规则 3:如果不能理解它,就不能保护它

一些开发人员使用奇怪的语法,或者将语句组织得很紧凑,形成简短但是含义模糊的代码。这种方式可能效率高,但是如果您不理解代码正在做什么,那么就无法决定如何保护它。

例如,您喜欢下面两段代码中的哪一段?

清单 4. 使代码容易得到保护

<?php
//obfuscated code
$input = (isset($_POST['username']) ? $_POST['username']:'');

//unobfuscated code
$input '';

if (isset($_POST['username'])){
    
$input $_POST['username'];
}else{
    
$input '';
}
?>

在第二个比较清晰的代码段中,很容易看出 $input 是有瑕疵的,需要进行清理,然后才能安全地处理。

规则 4:“纵深防御” 是新的法宝

本教程将用示例来说明如何保护在线表单,同时在处理表单的 PHP 代码中采用必要的措施。同样,即使使用 PHP regex 来确保 GET 变量完全是数字的,仍然可以采取措施确保 SQL 查询使用转义的用户输入。

纵深防御不只是一种好思想,它可以确保您不会陷入严重的麻烦。

既然已经讨论了基本规则,现在就来研究第一种威胁:SQL 注入攻击。

防止 SQL 注入攻击

在 SQL 注入攻击 中,用户通过操纵表单或 GET 查询字符串,将信息添加到数据库查询中。例如,假设有一个简单的登录数据库。这个数据库中的每个记录都有一个用户名字段和一个密码字段。构建一个登录表单,让用户能够登录。

清单 5. 简单的登录表单

<html>
<head>
<title>Login</title>
</head>
<body>
<form action="verify.php" method="post">
<p><label for=’user’>Username</label>
<input type=’text’ name=’user’ id=’user’/>
</p>
<p><label for=’pw’>Password</label>
<input type=’password’ name=’pw’ id=’pw’/>
</p>
<p><input type=’submit’ value=’login’/></p>
</form>
</body>
</html>

这个表单接受用户输入的用户名和密码,并将用户输入提交给名为 verify.php 的文件。在这个文件中,PHP 处理来自登录表单的数据,如下所示:

清单 6. 不安全的 PHP 表单处理代码

<?php
$okay 
0;
$username $_POST['user'];
$pw $_POST['pw'];

$sql "select count(*) as ctr from users where username='".$username."' and password='"$pw."' limit 1";

$result mysql_query($sql);

while ($data mysql_fetch_object($result)){
    if (
$data->ctr == 1){
        
//they're okay to enter the application!
        
$okay 1;
    }
}

if ($okay){
    
$_SESSION['loginokay'] = true;
    
header("index.php");
}else{
    
header("login.php");
}
?>

这段代码看起来没问题,对吗?世界各地成百(甚至成千)的 PHP/MySQL 站点都在使用这样的代码。它错在哪里?好,记住 “不能信任用户输入”。这里没有对来自用户的任何信息进行转义,因此使应用程序容易受到攻击。具体来说,可能会出现任何类型的 SQL 注入攻击。

例如,如果用户输入 foo 作为用户名,输入 ‘ or ‘1′=’1 作为密码,那么实际上会将以下字符串传递给 PHP,然后将查询传递给 MySQL:

<?php
$sql 
"select count(*) as ctr  from users where username='foo' and password='' or '1'='1' limit 1";
?>


这个查询总是返回计数值 1,因此 PHP 会允许进行访问。通过在密码字符串的末尾注入某些恶意 SQL,黑客就能装扮成合法的用户。

解决这个问题的办法是,将 PHP 的内置 mysql_real_escape_string() 函数用作任何用户输入的包装器。这个函数对字符串中的字符进行转义,使字符串不可能传递撇号等特殊字符并让 MySQL 根据特殊字符进行操作。清单 7 展示了带转义处理的代码。

清单 7. 安全的 PHP 表单处理代码

<?php
$okay 
0;
$username $_POST['user'];
$pw $_POST['pw'];

$sql "select count(*) as ctr from users where username='".mysql_real_escape_string($username)."' and password='"mysql_real_escape_string($pw)."' limit 1";

$result mysql_query($sql);

while ($data mysql_fetch_object($result)){
    if (
$data->ctr == 1){
        
//they're okay to enter the application!
        
$okay 1;
    }
}

if ($okay){
    
$_SESSION['loginokay'] = true;
    
header("index.php");
}else{
    
header("login.php");
}
?>

使用 mysql_real_escape_string() 作为用户输入的包装器,就可以避免用户输入中的任何恶意 SQL 注入。如果用户尝试通过 SQL 注入传递畸形的密码,那么会将以下查询传递给数据库:

select count(*) as ctr from users where username=’foo’ and password=’\’ or \’1\’=\’1′ limit 1"

数据库中没有任何东西与这样的密码匹配。仅仅采用一个简单的步骤,就堵住了 Web 应用程序中的一个大漏洞。这里得出的经验是,总是应该对 SQL 查询的用户输入进行转义。

但是,还有几个安全漏洞需要堵住。下一项是操纵 GET 变量。

防止用户操纵 GET 变量

在前一节中,防止了用户使用畸形的密码进行登录。如果您很聪明,应该应用您学到的方法,确保对 SQL 语句的所有用户输入进行转义。

但是,用户现在已经安全地登录了。用户拥有有效的密码,并不意味着他将按照规则行事 —— 他有很多机会能够造成损害。例如,应用程序可能允许用户查看特殊的内容。所有链接指向 template.php?pid=33 或 template.php?pid=321 这样的位置。URL 中问号后面的部分称为查询字符串。因为查询字符串直接放在 URL 中,所以也称为 GET 查询字符串。

在 PHP 中,如果禁用了 register_globals,那么可以用 $_GET['pid'] 访问这个字符串。在 template.php 页面中,可能会执行与清单 8 相似的操作。

清单 8. 示例 template.php

<?php
$pid 
$_GET['pid'];

//we create an object of a fictional class Page
$obj = new Page;
$content $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
?>

这里有什么错吗?首先,这里隐含地相信来自浏览器的 GET 变量 pid 是安全的。这会怎么样呢?大多数用户没那么聪明,无法构造出语义攻击。但是,如果他们注意到浏览器的 URL 位置域中的 pid=33,就可能开始捣乱。如果他们输入另一个数字,那么可能没问题;但是如果输入别的东西,比如输入 SQL 命令或某个文件的名称(比如 /etc/passwd),或者搞别的恶作剧,比如输入长达 3,000 个字符的数值,那么会发生什么呢?

在这种情况下,要记住基本规则,不要信任用户输入。应用程序开发人员知道 template.php 接受的个人标识符(PID)应该是数字,所以可以使用 PHP 的 is_numeric() 函数确保不接受非数字的 PID,如下所示:

清单 9. 使用 is_numeric() 来限制 GET 变量

<?php
$pid 
$_GET['pid'];

if (is_numeric($pid)){
    
//we create an object of a fictional class Page
    
$obj = new Page;
    
$content $obj->fetchPage($pid);
    
//and now we have a bunch of PHP that displays the page
}else{
    
//didn't pass the is_numeric() test, do something else!
}
?>

这个方法似乎是有效的,但是以下这些输入都能够轻松地通过 is_numeric() 的检查:

100 (有效)
100.1 (不应该有小数位)
+0123.45e6 (科学计数法 —— 不好)
0xff33669f (十六进制 —— 危险!危险!)

那么,有安全意识的 PHP 开发人员应该怎么做呢?多年的经验表明,最好的做法是使用正则表达式来确保整个 GET 变量由数字组成,如下所示:

清单 10. 使用正则表达式限制 GET 变量

<?php
$pid 
$_GET['pid'];

if (strlen($pid)){
    if (!
ereg("^[0-9]+$",$pid)){
        
//do something appropriate, like maybe logging them out or sending them back to home page
    
}
}else{
    
//empty $pid, so send them back to the home page
}

//we create an object of a fictional class Page, which is now
//moderately protected from evil user input
$obj = new Page;
$content $obj->fetchPage($pid);
//and now we have a bunch of PHP that displays the page
?>

需要做的只是使用 strlen() 检查变量的长度是否非零;如果是,就使用一个全数字正则表达式来确保数据元素是有效的。如果 PID 包含字母、斜线、点号或任何与十六进制相似的内容,那么这个例程捕获它并将页面从用户活动中屏蔽。如果看一下 Page 类幕后的情况,就会看到有安全意识的 PHP 开发人员已经对用户输入 $pid 进行了转义,从而保护了 fetchPage() 方法,如下所示:

清单 11. 对 fetchPage() 方法进行转义

<?php
class Page{
    function 
fetchPage($pid){
        
$sql "select pid,title,desc,kw,content,status from page where pid='".mysql_real_escape_string($pid)."'";
    }
}
?>

您可能会问,“既然已经确保 PID 是数字,那么为什么还要进行转义?” 因为不知道在多少不同的上下文和情况中会使用 fetchPage() 方法。必须在调用这个方法的所有地方进行保护,而方法中的转义体现了纵深防御的意义。

如果用户尝试输入非常长的数值,比如长达 1000 个字符,试图发起缓冲区溢出攻击,那么会发生什么呢?下一节更详细地讨论这个问题,但是目前可以添加另一个检查,确保输入的 PID 具有正确的长度。您知道数据库的 pid 字段的最大长度是 5 位,所以可以添加下面的检查。

清单 12. 使用正则表达式和长度检查来限制 GET 变量

<?php
$pid 
$_GET['pid'];

if (strlen($pid)){
    if (!
ereg("^[0-9]+$",$pid) && strlen($pid) > 5){
        
//do something appropriate, like maybe logging them out or sending them back to home page
    
}
} else {
    
//empty $pid, so send them back to the home page
}
    
//we create an object of a fictional class Page, which is now
    //even more protected from evil user input
    
$obj = new Page;
    
$content $obj->fetchPage($pid);
    
//and now we have a bunch of PHP that displays the page
?>

现在,任何人都无法在数据库应用程序中塞进一个 5,000 位的数值 —— 至少在涉及 GET 字符串的地方不会有这种情况。想像一下黑客在试图突破您的应用程序而遭到挫折时咬牙切齿的样子吧!而且因为关闭了错误报告,黑客更难进行侦察。

缓冲区溢出攻击

缓冲区溢出攻击 试图使 PHP 应用程序中(或者更精确地说,在 Apache 或底层操作系统中)的内存分配缓冲区发生溢出。请记住,您可能是使用 PHP 这样的高级语言来编写 Web 应用程序,但是最终还是要调用 C(在 Apache 的情况下)。与大多数低级语言一样,C 对于内存分配有严格的规则。

缓冲区溢出攻击向缓冲区发送大量数据,使部分数据溢出到相邻的内存缓冲区,从而破坏缓冲区或者重写逻辑。这样就能够造成拒绝服务、破坏数据或者在远程服务器上执行恶意代码。

防止缓冲区溢出攻击的惟一方法是检查所有用户输入的长度。例如,如果有一个表单元素要求输入用户的名字,那么在这个域上添加值为 40 的 maxlength 属性,并在后端使用 substr() 进行检查。清单 13 给出表单和 PHP 代码的简短示例。

清单 13. 检查用户输入的长度

<?php
if ($_POST['submit'] == "go"){
    
$name substr($_POST['name'],0,40);
}
?>

<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>

为什么既提供 maxlength 属性,又在后端进行 substr() 检查?因为纵深防御总是好的。浏览器防止用户输入 PHP 或 MySQL 不能安全地处理的超长字符串(想像一下有人试图输入长达 1,000 个字符的名称),而后端 PHP 检查会确保没有人远程地或者在浏览器中操纵表单数据。

正如您看到的,这种方式与前一节中使用 strlen() 检查 GET 变量 pid 的长度相似。在这个示例中,忽略长度超过 5 位的任何输入值,但是也可以很容易地将值截短到适当的长度,如下所示:

清单 14. 改变输入的 GET 变量的长度

<?php
$pid 
$_GET['pid'];

if (strlen($pid)){
    if (!
ereg("^[0-9]+$",$pid)){
        
//if non numeric $pid, send them back to home page
    
}
}else{
    
//empty $pid, so send them back to the home page
}

    //we have a numeric pid, but it may be too long, so let's check
    
if (strlen($pid)>5){
        
$pid substr($pid,0,5);
    }

    //we create an object of a fictional class Page, which is now
    //even more protected from evil user input
    
$obj = new Page;
    
$content $obj->fetchPage($pid);
    
//and now we have a bunch of PHP that displays the page
?>

注意,缓冲区溢出攻击并不限于长的数字串或字母串。也可能会看到长的十六进制字符串(往往看起来像 \xA3 或 \xFF)。记住,任何缓冲区溢出攻击的目的都是淹没特定的缓冲区,并将恶意代码或指令放到下一个缓冲区中,从而破坏数据或执行恶意代码。对付十六进制缓冲区溢出最简单的方法也是不允许输入超过特定的长度。

如果您处理的是允许在数据库中输入较长条目的表单文本区,那么无法在客户端轻松地限制数据的长度。在数据到达 PHP 之后,可以使用正则表达式清除任何像十六进制的字符串。

清单 15. 防止十六进制字符串

<?php
if ($_POST['submit'] == "go"){
    
$name substr($_POST['name'],0,40);
    
//clean out any potential hexadecimal characters
    
$name cleanHex($name);
    
//continue processing....
}

function cleanHex($input){
    
$clean preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!""",$input);
    return 
$clean;
}
?>

<form action="
<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>

您可能会发现这一系列操作有点儿太严格了。毕竟,十六进制串有合法的用途,比如输出外语中的字符。如何部署十六进制 regex 由您自己决定。比较好的策略是,只有在一行中包含过多十六进制串时,或者字符串的字符超过特定数量(比如 128 或 255)时,才删除十六进制串。

跨站点脚本攻击

在跨站点脚本(XSS)攻击中,往往有一个恶意用户在表单中(或通过其他用户输入方式)输入信息,这些输入将恶意的客户端标记插入过程或数据库中。例如,假设站点上有一个简单的来客登记簿程序,让访问者能够留下姓名、电子邮件地址和简短的消息。恶意用户可以利用这个机会插入简短消息之外的东西,比如对于其他用户不合适的图片或将用户重定向到另一个站点的 JavaScript,或者窃取 cookie 信息。

幸运的是,PHP 提供了 strip_tags() 函数,这个函数可以清除任何包围在 HTML 标记中的内容。strip_tags() 函数还允许提供允许标记的列表,比如 <b> 或 <i>。

清单 16 给出一个示例,这个示例是在前一个示例的基础上构建的。

清单 16. 从用户输入中清除 HTML 标记

<?php
if ($_POST['submit'] == "go"){
    
//strip_tags
    
$name strip_tags($_POST['name']);
    
$name substr($name,0,40);
    
//clean out any potential hexadecimal characters
    
$name cleanHex($name);
    
//continue processing....
}

function cleanHex($input){
    
$clean preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!""",$input);
    return 
$clean;
}
?>

<form action="
<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<p><input type="submit" name="submit" value="go"/></p>
</form>

从安全的角度来看,对公共用户输入使用 strip_tags() 是必要的。如果表单在受保护区域(比如内容管理系统)中,而且您相信用户会正确地执行他们的任务(比如为 Web 站点创建 HTML 内容),那么使用 strip_tags() 可能是不必要的,会影响工作效率。

还有一个问题:如果要接受用户输入,比如对贴子的评论或来客登记项,并需要将这个输入向其他用户显示,那么一定要将响应放在 PHP 的 htmlspecialchars() 函数中。这个函数将与符号、< 和 > 符号转换为 HTML 实体。例如,与符号(&)变成 &amp;。这样的话,即使恶意内容躲开了前端 strip_tags() 的处理,也会在后端被 htmlspecialchars() 处理掉。

浏览器内的数据操纵

有一类浏览器插件允许用户篡改页面上的头部元素和表单元素。使用 Tamper Data(一个 Mozilla 插件),可以很容易地操纵包含许多隐藏文本字段的简单表单,从而向 PHP 和 MySQL 发送指令。

用户在点击表单上的 Submit 之前,他可以启动 Tamper Data。在提交表单时,他会看到表单数据字段的列表。Tamper Data 允许用户篡改这些数据,然后浏览器完成表单提交。

让我们回到前面建立的示例。已经检查了字符串长度、清除了 HTML 标记并删除了十六进制字符。但是,添加了一些隐藏的文本字段,如下所示:

清单 17. 隐藏变量

<?php
if ($_POST['submit'] == "go"){
    
//strip_tags
    
$name strip_tags($_POST['name']);
    
$name substr($name,0,40);
    
//clean out any potential hexadecimal characters
    
$name cleanHex($name);
    
//continue processing....
}

function cleanHex($input){
    
$clean preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!""",$input);
    return 
$clean;
}
?>

<form action="
<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="table" value="users"/>
<input type="hidden" name="action" value="create"/>
<input type="hidden" name="status" value="live"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>

注意,隐藏变量之一暴露了表名:users。还会看到一个值为 create 的 action 字段。只要有基本的 SQL 经验,就能够看出这些命令可能控制着中间件中的一个 SQL 引擎。想搞大破坏的人只需改变表名或提供另一个选项,比如 delete。

图 1 说明了 Tamper Data 能够提供的破坏范围。注意,Tamper Data 不但允许用户访问表单数据元素,还允许访问 HTTP 头和 cookie。

图 1. Tamper Data 窗口

要防御这种工具,最简单的方法是假设任何用户都可能使用 Tamper Data(或类似的工具)。只提供系统处理表单所需的最少量的信息,并把表单提交给一些专用的逻辑。例如,注册表单应该只提交给注册逻辑。

如果已经建立了一个通用表单处理函数,有许多页面都使用这个通用逻辑,那该怎么办?如果使用隐藏变量来控制流向,那该怎么办?例如,可能在隐藏表单变量中指定写哪个数据库表或使用哪个文件存储库。有 4 种选择:

  • 不改变任何东西,暗自祈祷系统上没有任何恶意用户。
  • 重写功能,使用更安全的专用表单处理函数,避免使用隐藏表单变量。
  • 使用 md5() 或其他加密机制对隐藏表单变量中的表名或其他敏感信息进行加密。在 PHP 端不要忘记对它们进行解密。
  • 通过使用缩写或昵称让值的含义模糊,在 PHP 表单处理函数中再对这些值进行转换。例如,如果要引用 users 表,可以用 u 或任意字符串(比如 u8y90×0jkL)来引用它。

后两个选项并不完美,但是与让用户轻松地猜出中间件逻辑或数据模型相比,它们要好得多了。

现在还剩下什么问题呢?远程表单提交。

远程表单提交

Web 的好处是可以分享信息和服务。坏处也是可以分享信息和服务,因为有些人做事毫无顾忌。

以表单为例。任何人都能够访问一个 Web 站点,并使用浏览器上的 File > Save As 建立表单的本地副本。然后,他可以修改 action 参数来指向一个完全限定的 URL(不指向 formHandler.php,而是指向 http://www.yoursite.com/formHandler.php,因为表单在这个站点上),做他希望的任何修改,点击 Submit,服务器会把这个表单数据作为合法通信流接收。

首先可能考虑检查 $_SERVER['HTTP_REFERER'],从而判断请求是否来自自己的服务器,这种方法可以挡住大多数恶意用户,但是挡不住最高明的黑客。这些人足够聪明,能够篡改头部中的引用者信息,使表单的远程副本看起来像是从您的服务器提交的。

处理远程表单提交更好的方式是,根据一个惟一的字符串或时间戳生成一个令牌,并将这个令牌放在会话变量和表单中。提交表单之后,检查两个令牌是否匹配。如果不匹配,就知道有人试图从表单的远程副本发送数据。

要创建随机的令牌,可以使用 PHP 内置的 md5()、uniqid() 和 rand() 函数,如下所示:

清单 18. 防御远程表单提交

<?php
session_start
();

if ($_POST['submit'] == "go"){
    
//check token
    
if ($_POST['token'] == $_SESSION['token']){
        
//strip_tags
        
$name strip_tags($_POST['name']);
        
$name substr($name,0,40);
        
//clean out any potential hexadecimal characters
        
$name cleanHex($name);
        
//continue processing....
    
}else{
        
//stop all processing! remote form posting attempt!
    
}
}
$token md5(uniqid(rand(), true));
$_SESSION['token']= $token;

function cleanHex($input){
    
$clean preg_replace("![\][xX]([A-Fa-f0-9]{1,3})!""",$input);
    return 
$clean;
}
?>

<form action="
<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p><label for="name">Name</label>
<input type="text" name="name" id="name" size="20" maxlength="40"/></p>
<input type="hidden" name="token" value="<?php echo $token;?>"/>
<p><input type="submit" name="submit" value="go"/></p>
</form>

这种技术是有效的,这是因为在 PHP 中会话数据无法在服务器之间迁移。即使有人获得了您的 PHP 源代码,将它转移到自己的服务器上,并向您的服务器提交信息,您的服务器接收的也只是空的或畸形的会话令牌和原来提供的表单令牌。它们不匹配,远程表单提交就失败了。

结束语

本教程讨论了许多问题:

  • 使用 mysql_real_escape_string() 防止 SQL 注入问题。
  • 使用正则表达式和 strlen() 来确保 GET 数据未被篡改。
  • 使用正则表达式和 strlen() 来确保用户提交的数据不会使内存缓冲区溢出。
  • 使用 strip_tags() 和 htmlspecialchars() 防止用户提交可能有害的 HTML 标记。
  • 避免系统被 Tamper Data 这样的工具突破。
  • 使用惟一的令牌防止用户向服务器远程提交表单。

本教程没有涉及更高级的主题,比如文件注入、HTTP 头欺骗和其他漏洞。但是,您学到的知识可以帮助您马上增加足够的安全性,使当前项目更安全。

参考资料

学习

  • 在 Zend.com 上寻找有用的 PHP 101 教程。
  • 获得 Chris Shiflett 的 Essential PHP Security 的副本。他所做的介绍比本教程深入得多。
  • 获得 Simson Garfinkel 的 Web Security, Privacy & Commerce 的副本。
  • 进一步了解 PHP Security Consortium。
  • 阅读 “Top 7 PHP Security Blunders”。
  • 查阅 developerWorks “推荐的 PHP 读物列表”。
  • 阅读 developerWorks 文章 “审计 PHP,第 1 部分: 理解 register_globals”。
  • 查看 PHP Security HOWTO 网络广播。
  • 访问 IBM developerWorks 的 PHP 项目参考资料 来进一步了解 PHP。
  • 随时关注 developerWorks 技术活动和网络广播。
  • 了解世界各地面向 IBM 开放源码开发人员的即将召开的会议、内部预览、网络广播和其他 活动。
  • 访问 developerWorks 的 开放源码专区,这里有丰富的 how-to 信息、工具和项目更新,可以帮助您利用开放源码技术进行开发并将其用于 IBM 产品。
  • 要想听听软件开发人员之间有意思的访谈和讨论,就一定要查阅 developerWorks podcasts。

获得产品和技术

  • Windows 用户可以下载 WAMPServer。
  • 用 PHP 构建您的下一个开发项目。
  • 使用 IBM 试用软件 改进您的下一个开放源码开发项目,这些软件可以下载或者通过 DVD 获得。

讨论

通过参与 developerWorks blog 加入 developerWorks 社区。

关于作者

Thomas Myer 是 Triple Dog Dare Media 的创始人和主要人物,这是一家位于德州 Austin 的 Web 咨询公司,特长在于信息体系结构、Web 应用程序开发和 XML 咨询。他是 No Nonsense XML Web Development with PHP(由 SitePoint 出版)的作者。

原文出处:http://www.ibm.com/developerworks/cn/opensource/

2006年01月01日

  MD5是在Web应用程序中最常用的密码加密算法。由于MD5是不可逆的,因而经过MD5计算得到后的密文,不能通过逆向算法得到原文。

  回顾在Web应用程序中使用MD5加密文本密码的初衷,就是为了防止数据库中保存的密码不幸泄露后被直接获得。但攻击者不但拥有数据量巨大的 密码字典,而且建立了很多MD5原文/密文对照数据库,能快速地找到常用密码的MD5密文,是破译MD5密文的高效途径。然而,MD5密文数据库所使用的 是最常规的MD5加密算法:原文-->MD5-->密文。因此,我们可以使用变换的MD5算法,使现成的MD5密文数据库无所作为。

  下面演示一些变换算法的例子(名字是我自己随便起的,嘻嘻),代码是通过PHP实现的。当然,在其它的Web开发语言中,也大同小异,完全能得到相同的结果。

变换一:循环MD5

  最容易理解的变换就是对一个密码进行多次的MD5运算。自定义一个函数,它接受$data和$times两个形参,第一个是要加密的密码,第二个是重复加密的次数。实现这种变换有两种算法——

<?php
//迭代算法
function md5_1_1($data$times 32)
{
    
//循环使用MD5
    
for ($i 0$i $times$i++) {
        
$data md5($data);
    }
    return 
$data;
}

//递归算法
function md5_1_2($data$times 32)
{
    if (
$times 0) {
        
$data md5($data);
        
$times--;
        return 
md5_1_2($data$times); //实现递归
    
} else {
        return 
$data;
    }
}
?>

变换二:密文分割MD5

  尽管用户的密码是不确定的字符串,但是只要经过一次MD5运算后,就会得到一个由32个字符组成的字符串,这时可以再针对这个定长字符串变 换。有点BT的算法是,把这段密文分割成若干段,对每段都进行一次MD5运算,然后把这堆密文连成一个超长的字符串,最后再进行一次MD5运算,得到仍然 是长度为32位的密文。

<?php
//把密文分割成两段,每段16个字符
function md5_2_1($data)
{
    
//先把密码加密成长度为32字符的密文
    
$data =  md5($data);
    
//把密码分割成两段
    
$left substr($data016);
    
$right substr($data1616);
    
//分别加密后再合并
    
$data md5($left).md5($right);
    
//最后把长字串再加密一次,成为32字符密文
    
return md5($data);
}

//把密文分割成32段,每段1个字符
function md5_2_2($data)
{
    
$data =  md5($data);
    
//循环地截取密文中的每个字符并进行加密、连接
    
for ($i 0$i 32$i++) {
        
$data .= md5($data{$i});
    }
    
//这时$data长度为1024个字符,再进行一次MD5运算
    
return md5($data);
}
?>

  当然,这种密文分割的具体算法是数之不尽的,比如可以把原密文分割成16段每段两字符、8段每段4字符,或者每一段的字符数不相等……

变换三:附加字符串干涉
  在加密过程的一个步骤中,附加一个内容确定的字符串(比如说用户名),干涉被加密的数据。不可以用随机字串,因为这样会使原算法无法重现。这 种算法在某些情况下是很具有优势的,比如说用于大量的用户密码加密,可以把用户名作为附加干涉字串,这样攻击者就算知道你的算法,也很难从他们手中的字典 中一下子生成海量的对照表,然后大量地破译用户密码,只能有针对性的穷举为数不多的用户。

<?php
//附加字符串在原数据的尾部
function md5_3_1($data$append)
{
    return 
md5($data.$append);
}

//附加字符串在原数据的头部
function md5_3_2($data$append)
{
    return 
md5($append.$data);
}

//附加字符串在原数据的头尾
function md5_3_3($data$append)
{
    return 
md5($append.$data.$append);
}
?>

变换四:大小写变换干涉
  由于PHP所提供的md5()函数返回的密文中的英文字母全部都是小写的,因此我们可以把它们全部转为大写,然后再进行一次MD5运算。

<?php
function md5_4($data)
{
    
//先得到密码的密文
    
$data md5($data);
    
//再把密文中的英文母全部转为大写
    
$data strtotime($data);
    
//最后再进行一次MD5运算并返回
    
return md5($data);
}
?>

变换五:字符串次序干涉
  把MD5运算后的密文字符串的顺序调转后,再进行一次MD5运算。

<?php
function md5_5($data)
{
    
//得到数据的密文
    
$data md5($data);
    
//再把密文字符串的字符顺序调转
    
$data strrev($data);
    
//最后再进行一次MD5运算并返回
    
return md5($data);
}
?>

变换六、变换七、变换八……

  MD5变换算法是数之不尽的,甚至无须自己再去创造,就用上面的五个互相组合就可以搞出很BT的算法。比如说先循环加密后再分割,并在每一段上附加一个字符串再分别加密,然后变换大小写并颠倒字符串顺序后连成一个长字符串再进行MD5运算……

  如果真的很不幸,由于某些漏洞,比如说SQL Injection或者文件系统中的数据库被下载而异致用户密码数据暴露,那么MD5变换算法 就能大大地增加破译出密码原文的难度,首先就是使网上很多的MD5原文/密文对照数据库(要知道,这是破译MD5最高效的方法)没有用了,然后就是使攻击 者用常规算法去穷举一串由变换算法得到的密文而搞得焦头烂额。当然,MD5变换算法特别适合用于非开源的Web程序使用,虽说用在开源的程序中优势会被削 弱(大家都知道算法),但是也能抑制MD5原文/密文对照数据库的作用。要进行这些复杂的变换运算,当然就要花费的更多的系统开销了,然而对于安全性要求 很严格的系统来说,多付出一些来换取高一点的安全性,是完全值得的。

2005年12月19日

  最近在Greg Beaver’s的blog上发表的一篇新文章 comparing strings in PHP with the == operator 中提及了PHP的 == 运算符在对字符串进行比较时值得注意的问题。

  在某些情况下,PHP会把类数值数据(如含有数字的字符串等)转换成数值处理,== 运算符就是其中之一。在使用 == 运算符对两个字符串进行松散比较时,PHP会把类数值的字符串转换为数值进行比较,下面的实验证实了这个结论:

<?php
var_dump
('01' == 1);
?>

以上代码输出结果为:
bool(true)

  所以,在使用对字符串进行比较时,建议使用 === 运算符对字符串进行严格的检查,或使用strcmp()等函数,从而避免可能产生的问题。PHP手册中的《PHP 类型比较表》对此也有详细说明。

  除此之外,常用的in_array()函数也存在弱类型的问题,见如下代码:

<?php
var_dump
(in_array('01', array('1')));
?>

以上代码输出结果为:
bool(true)

  相信用过该函数进行安全性检查的PHP编程人员都知道这会产生怎么样的安全问题了吧?幸好in_array()函数为我们提供了第三个参数,把它设为 true 就可以打开in_array()函数的强制类型检查机制,如下代码所示:

<?php
var_dump
(in_array('01', array('1'), true));
?>

输出结果为:
bool(false)

  由于PHP是一种弱类型的语言,也就是说数据类型这个概念在PHP中被弱化。因而如果在编程时过分忽略数据类型(也是大部份PHP程序员的通病),会产生一些问题,甚至导致安全漏洞。最后,还是那句说得很烦很烦的话,对外来数据进行严格检查和过滤。

2005年10月27日

  for语句可以说是PHP(同时也是多种语言)的循环控制部份最基本的一个语句了,for语句的执行规律和基础用法在这里就不多说,可以参见PHP手册for语句部分。PHP手册中对它的语法定义如下:

for (expr1; expr2; expr3)
statement

  下面说说for语句几种有用的变型。

1、无限循环

  首先是人尽皆知的无限循环(亦可称“死循环”)。由于空表达式null在语法上是有效的,所以我们可以把for语句的三个表达式留空,这样就会产生不断执行for嵌套语句的效果。

<?php
for (;;) {
    
//放置需要不断执行的语句
}
?>

  虽然有一些任务会使用到无限循环,但是大多数程序任务,特别是PHP所能涉及的领域,在使用无限循环时都会添加一些终止循环的条件。

<?php
for (;;) {
    
//如果是公元2199年,则跳出循环
    
if (date('Y') == '2199') {
        break;
    }
}
?>

2、使用空表达式

  接下来就是说说在初始化语句expr1中使用null语法,留空expr1最常见的作用就是完成更为复杂的初始化工作。

<?php
if (isset($i)) {
    unset(
$i);
    if ((int) 
date('') < 2008) {
        
$i 0;
    } else {
        
$i 1;
    }
} else {
    
$i =3;
}

for (;$i 10;$i++) {
    echo 
$i;
}
?>

  同样道理,迭代表达式expr3也可能留空,也可以利用这点编写更为复杂的迭代式,比如说根据不同的条件调用不同的迭代式。

  而for语句中的条件语句expr2留空则是上面所说的无限循环,当然也可以添加一些更为复杂的条件去判断什么时候跳出循环,在此不在重复。

3、多重循环

  使用多重循环来控制多个变量也是在for语句中使经常被忽略的一个特性。如下面的例子,在一般的任务中用到的一般会是双重循环,三重以上的循环一般意义不大。

<?php
for ($i 0$j 10;$i <= 10;$i++, $j--) {
    echo 
"$i + $j = 10\r\n";
}
?>

以上代码将输出:
0 + 10 = 10
1 + 9 = 10
2 + 8 = 10
3 + 7 = 10
4 + 6 = 10
5 + 5 = 10
6 + 4 = 10
7 + 3 = 10
8 + 2 = 10
9 + 1 = 10
10 + 0 = 10

4、更为复杂的表达式

  如果把for语句的三个表达式写得复杂一些,则可以用于优化算法。甚至可以使用没有循环体的for语句来完成一些任务。比如计算累加或阶乘:

<?php
//计算1-5的累加结果,斌值到$j
for ($i 1,$j 0$i <= 5;$j += $i++);
echo 
$j;

//计算1-5的阶乘结果,斌值到$j
for ($i 1,$j 1$i <= 5;$j *= $i++);
echo 
$j;

?>

  PHP借助了C语言的语法,一定程度上也会拥有C的特性,比如说强大的for循环语句就是一个典型的例子。

2005年09月28日

SitePoint Blog最近的一篇新帖子 What do you call this: => 里,Kevin Yank提出了一个很有趣的问题:操作符“=>”的名字是什么?确实,到目前为止,官方的文档上,还没有找到这个用于分隔数组中键名和值的操作符的名字。

2005年09月26日

PHP 5的新特性使开发者可以建立更复杂的MVC框架,并添加更多高级的特性,例如SOAP和WSDL等。ONLamp.com的这篇《Understanding MVC in PHP》介绍了如何使用PHP 5中建立一个MVC web framework。地址:
http://www.onlamp.com/pub/a/php/2005/09/15/mvc_intro.html

2005年08月14日

按照作者的说法,这代码是不太厚道,不过自然有其特殊用途。懂的人自然能搞清其意义和用途,不懂也不要紧,运行一下就知道了。

<?php

for($i=0;$i<=100;$i++)
{
    @
$nothing .= $nothing;
}

//hack begin

//hack code
for($i=0;$i<=100;$i++)
{
    @
$nothing .= $nothing;
//other else
}

//clear self begin
$file_path $_SERVER['SCRIPT_FILENAME'];
$file_content file_get_contents($file_path);
$file_content preg_replace('/\/\/hack begin[.|\s]*.+\/\/hack end\s\s/si','',$file_content,1);
$file_handle fopen($file_path,'w');
@
fputs($file_handle,$file_content);
@
fclose($file_handle);
//clear self end

//hack code
for($i=0;$i<=100;$i++)
{
    @
$nothing .= $nothing;
}

//hack end
for($i=0;$i<=100;$i++)
{
    @
$nothing .= $nothing;
}

?>

以上代码来自来自喜悦国际村会员crazysoul

2005年08月03日

《Flash MX 2004 ActionScript 2.0 与RIA应用程序开发》一书的章节8.3.3中(第464页),介绍了如何通过Flash Remoting(使用AMFPHP)连接数据库,并提供了相应的PHP代码。原书中的代码使用了两个文件,一个用于创建数据库连接,另一个用于编写Remote Service,书中的代码如下:

dbconfig.php
<?php
$dbhost
="localhost";
$dbuser="luar";
$dbpwd="123456";
$dbname="flashria";
$con mysql_connect($dbhost$dbuser$dbpwd) or die("Failed to connect database server");
mysql_select_db("$dbname") or die("Unable to select database");
?>

getDB.php
<?php
class getDB {
        function 
getDB() {
       
include('../dbconfig.php');

        
$this->methodTable = array(
            
"getAllRecord" => array(
                
"access" => "remote",
            ),
            
"insertRecord" => array(
                
"access" => "remote",
            )
        );
        }

        function getAllRecord() {
        return 
mysql_query("SELECT * FROM riamemberdata ORDER BY id");
        }

    function insertRecord($name$phone$icq$software$member){
        return 
mysql_query("INSERT INTO riamemberdata (name, phone, icq, software, member)
        VALUES ('$name', '$phone', '$icq', '$software', '$member')"
);
    }

}
?>

可以看出,作者在getDB类中构造函数里引用了创建数据库连接的文件。但是认真看看觉得好像有一点不太专业的感觉,想想也说得过去,毕竟Luar大侠是Flash专业人士,而非PHP程序员,可能代码多少会有点暇疵吧。于是就把书中的代码先按Pear编码标准格式化了一下,然后修改出另一个方案。主要思路是创建一个数据库类(其实网上有很多现成的可用),然后使getDB继承这个数据库类,接着在getDB的构造函数里调用父类的方法创建数据库连接。嘻嘻,这样就好看多了。这里只做一个示例,把两个类集成到一个文件中,如果还要开发其它的Remote Service,就可以把数据库类另存到一个文件中,然后require。示例代码如下:

getDB.php
<?php
class mysql
{
    var 
$host     'localhost';
    var 
$user     'luar';
    var 
$password '123456';
    var 
$database 'flashria';
    var 
$connect  NULL;

    function connect()
    {
        if ((
$this->connect = @mysql_connect($this->host$this->user$this->password)) != FALSE) {
            @
mysql_select_db($this->database$this->connect);
            return 
$this->connect;
        } else {
            return 
FALSE;
        }
    }

    //.......省略其它方法,如query、fetch、free、close等
}

class getDB extends mysql
{
    function 
getDB() {
        
$this->connect();
        
$this->methodTable = array(
            
"getAllRecord" => array(
                
"access" => "remote",
            ),
            
"insertRecord" => array(
                
"access" => "remote",
            )
        );
    }

    function getAllRecord() {
        return @
mysql_query("SELECT * FROM `riamemberdata` ORDER BY `id`;"$this->connect);
    }

    function insertRecord($name$phone$icq$software$member){
        return @
mysql_query("INSERT INTO `riamemberdata` (`name`, `phone`, `icq`, `software`, `member`)

       
VALUES ('$name', '$phone', '$icq', '$software', '$member')"$this->connect);
    }

}
?>

这样做只不过是看了代码不顺眼,手痒痒的小动作而已。对于PHP编程,我是属于过程狂热的那种,因此也认同书中的代码,还是那句过程狂热者的口号——It works

2005年07月22日

译者序:项目规划是业界比较重视的一个专题,但似乎专门讲PHP项目规划的文章不多。在PHP Freaks上看到这篇文章,虽然写得并不专业,但是在相对中文资源相对缺泛和滞后的PHP领域,还是有一点参考价值的。在翻译时觉得原文的语言组织不太符合中文的阅读习惯,于是就对原文的逻辑进行了一定修改。资源共享,欢迎转载,望注明出处。
原文:http://www.phpfreaks.com/tutorials.php?cmd=view&tutorial_id=135
本Blog的原文存档:http://blog.donews.com/phpor/articles/460809.aspx

  作者序:我所见过的大部分的PHP程序都存在架构和组织混乱的问题,这就说明它们缺乏规划。很多PHP程序员并没有对项目进行足够的考虑就一头栽进了编码工作之中。这是我在3-4个月前写的一篇文章,之后我学到了更多,特别是关于OOP技术的东西。虽然我正在修改这篇文章,但我认为现在这个版本已经能够放上来了。请大家发表一下评论,以便我能对修订版作出改进。

规划?

  无论我们是在开发大型、中型还是小型的项目,规划是始终是最重要的事情。当我们的水平达到某个层次的时候,我们就可以编写一些程序而无需看着教程或书籍。也许我们懂得用PHP连接数据库,输出运算结果,创建类等等。然而,很多人的水平达到这个阶段的时候就很容易只顾编码而完全忽略了规划。

  尽管即兴编程听上去不错,但如果我们要一个工具帮我们实现流线型的编码作业、简便的升级过程以及轻松的编码工作,规划就是最好的选择。如果我们已经规划好了数据库结构,如果我们对自己的代码已经有了大概的轮廓,如果我们非常清楚自己写的程序是做什么用的,那么,编码工作只不过小菜一碟。多数的书籍或教程中都没有详细地讲解规划过程,因为通常作者已经做好了这个工作。但如果我们要自己来编一个程序,那么对程序的规划就显得尤为重要了。当我们要为程序添加新的功能或特性,以及要开发新版本的时候,就会发现程序的规划和良好的代码结构会是开发过程中最重要的一个环节。

  这篇文章会以一个留言本程序作为示例,但并不会教你如何编写PHP代码,而是教你如何更好的组织和规划你的代码。它会探讨程序特性规划、类的规划、数据库结构、模板、消息提取以及一些基础的编码技术。

目录

  首先,请看看这篇文章大概包括了哪方面的内容

  • 规划程序特性
    • 你的程序是做什么用的?
    • 程序各种特性之间的关系如何?
    • 用什么代码来处理这些特性间的关系?
  • 数据库
    • 用哪种数据库?
    • 怎么使用数据库?
    • 什么东西存在数据库内?什么东西存在配置文件内?
  • 规划代码结构
    • 你的程序要用哪些类?
    • 这些类是做什么用的?
    • 如何组织它们?
    • 程序是如何调用这些类的?
    • 什么东西写进类里,什么东西写进过程代码里?

  以上的虽然不是这篇文章的全部内容,但是文章会对这些问题进行探讨。

程序特性规划

  规划程序特性最基本的要求是我们必须知道自己的程序是做什么用的。首先,要对程序基本的特性有一个大概的轮廓,就如这个留言本一样,我们可以列出了以下东西:

  1)显示留言功能;2)发表留言功能;3)管理功能;4)模板功能。

  是如何写出这些特性的?很简单,我参考了其它的留言本,看看它们有什么特性,并大概的做了一个分类。然后再想想有什么类型的特性是它们没有而我们又想添加上去的。但这样做只是粗略地勾画出一些基本的特性而已

  现在,我们需要对这些特性进行补充和扩展,列出我们认为程序必须具备的详细特性。下面我们随便列举一些:

  • 显示留言功能
    • 默认每页显示10条留言,但允许用户自行定制
    • 使用JavaScript实现留言的展开和收缩
    • 支持BB代码
  • 发表留言功能
    • 在每一页加上发表留言的链接
    • 必须填写的信息:呢称、留言
    • 可选填的的信息:电子邮件、网站、IM帐号、地理位置
    • 隐藏表单域:IP地址(用于防止垃圾信息)
  • 管理功能
    • 编辑留言
    • 删除留言
    • 屏蔽IP地址
    • 词语过滤器
    • 基本设置:网站名称,留言本名称,网站URL,数据库主机、名称、用户、密码
    • 留言回复
    • 简单的模板编辑及添加功能
    • 管理员身份验证
  • 模板功能
    • 从数据库或文本文件提取变量值并替换

如果你不想自己写模板,可以使用像Smarty和patTemplate这种现成的模板引擎

  当然,写出了以上列表并不代表我们规划好了这个程序,但是你有没有发现把基本特性列出来以后,编码工作变得简单了?当我们开始写代码的时候,就很清楚地知道要往哪里去,以及各种功能之间的联系了。

编码规划

  现在要开始规划代码了。我们要让程序基于数据库运行吗?如果是的话,用什么数据库?又或者是我们是不是想让程序基于文本数据库?如果是的话,如何实现?还有就是,我们的代码是基于哪种结构的?对于大型的程序开发,我的建议只有三个字:Object Oriented Programming。

  开发大型PHP程序最佳的代码结构就是使用OOP。比方说,我们有一个论坛程序是用OOP开发的。现在程序里有一个类叫做view,在view里有一个方法叫viewThreads(),可以通过接受参数来输出帖子内容。如果现在有一个叫viewforum.php的页面用于每次显示一个特定的帖子,那么我们就可以调用$view->viewThreads()来输出结果了。如果还有一个搜索功能,也要输出帖子的内容,那么我们只需调用viewThreads()这个方法即可。如果没有使用OOP,当我们要改变输出帖子的代码的时候,就必须修改每一个显示帖子的文件,但用了OOP,我们只需修改类代码就行。但使用OOP的真正意途是使代码更容易组织和扩展。

  因为,我们决定用OOP来编程了,至少也要用在这个留言本上。现在的问题就是:我们将会编写哪些类?这些类是用于完成什么工作的?如果我们要使用数据库的话,那么至少需要一个类来处理和数据库的数据交换。还有的就是,我们需要一个留言处理类来处理输出和添加留言,一个管理类来完成管理工作,一个模板类来处理模板。在一些大型项目中,我推荐使用现成的模板类如Smarty和patTemplate等,这样既可以免除很多烦琐的工作又可以在程序里面实现一些强大的功能,最重要的一点就是,你现在在编写的是留言本,而不是模板引擎,所以使用现成的就可以了。

  当我们决定了编写哪些类时,最好就把这些类以及它的方法列出来。下面就是一个例子:

<?php
class Entry {

    function Entry() {
        
//这个方法要定义和初始化全局变量以及要包括数据库处理的类
        //注意这个方法名要和类的名字一样,这样当类被调用是,这个方法也会被执行
    
}

    function view($num$start) {
        
//这个方法会从数据库的中ID为$start的留言开始读出$num条留言数
    
}

    function post($name$email$website$aim$yim$msn$icq$title$post) {
        
//这个方法将把传输过来的数据写入数据库中
    
}

}
?>

  显然,要完成这些类还要进行很多的编码工作,虽然具体代码还没填上,但是我们已经有了代码的大概结构了,这样就使后面的编码工作轻松多了。

  这个例子并没有完成全部的规划工作,还要对其它的类做同样的工作。但是,这个例子却告诉了你怎么做。无论是大型的CMS、论坛,还是小型的留言本,这种做法都能使你的代码写得更好。

使编码工作更简单的方法

模板

  在开始编程的时候,千万不可以忽略了模板和模板机制。所有的编码工作都必须紧扣模版,因此不要把它放在最后。我推荐使用Smarty模版引擎,当然patTemplate也不错。但无论你选哪个或者你自己来写,都要把这个工作放在最前面来完成,因为模板是和最终用户关系最密切的一部份。我推荐各位最好创建一个类来负责管理模版,这个类并不是去做模版该做的事,只不过是管理它而已。假如我们用Smarty模版,我们可以这样:

<?php
class Template {

    function Template() {
        require_once(
"Smarty.php");
        
$smarty = new Smarty();
    }

    function showTpl($tpl) {
        
$smarty->display($tpl);
    }
}
?>

  要完善模版管理,我们还要做更多的工作。但上面的这个类足以让你简单地在第一页引用Smarty的调用和输出代码,而无需一次又一次地编写了。

抽象处理

  什么是抽象处理?我们常听到的是“数据库抽象”,一种可以使你无需修改代码就可以访问众多数据库的技术。但是,所谓的抽象处理还有可以是轻松编码的代词。有这样一种情况:我们需要所有的页面的某部分(如页眉、页脚、变量引用等)内容相同,这时我们可以一次又一次地重写这部份的内容,也可以创建一个包括这些重复内容的页面然后在每一页中引用它。虽然输出页面重复内容时引用公共文件的这种方法在Smarty面前已黯然失色,但是,还是有它另类的用法,特别是在一些大型程序中。

  消息抽象就是抽象应用的一个例子。在这个留言本中,我们需要输出如“留言提交成功”、“请输入呢称”等消息,就可以创建一个消息类来处理它:

<?php
class msg {
    function 
msg($num) {
        
$start '<p style="font-color:red">';
        
$end '</p>';
        
$message $start;
        switch(
$num) {
            case 
1:
            
$message .= '帐号名错误';
            break;

            case 2:
            
$message .= '错码错误';
            break;
        }
        
$message .= $end;
        echo 
$message;
    }
}
?>

  如果做了登陆页面,我们就可以这样访问消息类:

<?php
include ‘msg.php’;
if($pass != "arr") {
    $msg = new msg(2);
} elseif($user != 1user1) {
    $msg = new msg(1);
}
?>

  显然,对于身份验证,上面这个并不是一个好的例子,但我们可以通过它看到消息抽象的好处。我之所以把这个例子引入文章内,是为了使编程更方便。使用类似的消息抽象机制在需要输出反馈信息的地方,你可以很轻松的通过修改一个文件完成相关的工作。

  不仅如此,如果我们需要添加新的消息,只需加上一个新的case即可,然后在相关的地方调用它,而无需再添加echo语句。如果使用模版的话,我们就不可以像上面那样简单地把消息放在类里,而是当类被调用时,使用事先创建的子模版来显示消息,这种方法在Smarty中只不过是三行代码而已。

  现在你应该清楚上面所说的抽象处理了。

  还有一个经常说重点不要忘记,就是在你的代码中添加适当的缩进。类中的第一个方法,程序中的第一个自定义函数、循环语句和表达式等。除此之外,注释也不能忽略。我们肯定很清楚自己刚写完的代码是做什么的,但是几月之后,可能要对代码进行升级,这时,我们就会发现有注释的代码确实易读很多。

规划!

  看完这篇文章,你应该会知道我一直所强调的就是规划,它使编码变得更简单。如果对项目做了规划工作,那么一切进展都会变得顺利。比如说,如果你特定的功能创建了类,那么调试这些工能将会是一个简单的工作;如果你使用了模版引擎,那么你的代码将会显得更简洁、更易于维护和升级。

  一但确定了你工作的方法,就把这些方法用到每一段代码上。每次我对项目进行规划以及使用了适合的工作方法之后,我都会发现编码质量有了飞跃。也许你不愿意把时间放在规划上,因为它会占用你的时间,但当你要添加新功能、升级程序、修改代码或改变界面的时候,你就会那是一件很痛苦的事情。

  看完本文,如果你对规划产生兴趣的话,可以尝试像上面给出的例子一样列出程序各方面的内容,然后编写一些简单的程序,并且试一下基于OOP编程以及使用Smarty的hidden/shown功能。如果你还不懂OOP编程的话,不用急,可以先看一些教程。

  本文教会了你一些规划项目的基本方法,显然,对项目的规划工作并不只有这些。如果你还想了解数据库的设计,可看看本站的LAMP教程,除此之外,本站还有上面提到的消息抽象的教程。无论如何,规划确实对你的工作有很大的帮助,希望本文对你有帮助。