本文档最后一次于2013年3月8日审核。最后一次修改是在2013年3月8日。
这由我, Alex Cabal维护的。到现在我已经写了很长时间PHP代码了, 目前我运行Scribophile,为严肃作家提供的在线写作小组, Writerfolio, 为自由职业者提供的简单的在线写作文件夹, 和 Standard Ebooks, 有插画的出版物,无数字版权的公共领域电子图书 。 偶尔我会自由的去找那些让我感兴趣的项目和客户。
如果你认为我能够帮你些什么,或者有些关于本文的建议或勘误的话,那么写信给我。
PHP是一门复杂的语言,让你长年承受内心的迂回,弯曲,拉伸和打击。它自相矛盾有时候充满bug。每个版本都有其独特的特性,缺点以及怪癖,而且你很难追踪什么版本有什么问题。不难想见为什么有时候它会招致那么多怨恨。
尽管如此,它是时下web中最流行的语言。由于它的悠久历史,你能找到许多教程,关于一些基本事情的做法,如密码哈希(一次性加密)和数据库访问。问题在于从五篇教程里,关于某件事情你很有可能找到五种完全不同的方法。哪个方法才是“正确”的方法呢?其它的方法有瑕疵或者意想不到的问题吗?确实很难弄清楚,而且你会在网络里到处点击,以试图确定正确的答案。
那也是为什么新的PHP程序员经常会为其丑陋,过时或不安全代码受到批评的原因之一。如果第一次Google搜索结果是一篇传授5年前方法的4年前的文章,他们就不能帮助这些有所改观。
这篇文章试图做这些工作。它尝试将一系列基本的操作提示汇集起来,这些可以被认为是PHP中处理普遍的令人困惑的难题时的最佳实践。如果PHP中一个低等级的任务具有多个和令人困惑的方法,它就属于这里。
它是在面对PHP程序员可能会遇到的普通的低级任务时,由于PHP提供了 许多选择而不容易了解到的,最佳途径的建议指导。例如:对许许多多可能的PHP解决方案,连接到数据库是一个普通的任务,但这些方案并不都是好的——因此,这个问题包含在这篇文章中。
它是一系列短小,引导式的解决方案。你应该行动起来将例子运行于基础的配置环境,而且你应该自己研究从中找到对自己有用的东西。
它指出了我们所理解的最先进的PHP。但是,这也意味着如果你正使用较老版本的PHP,可能你就没有实现这些方案所需要的一些特性。
这是一篇动态文件,随着PHP的继续演进,我将努力保持相应更新。
这篇文章不是PHP教程。你应该在别的地方学习基础和语法。
它不是普通web应用问题的指南,比如cookie存储,缓存,代码风格,文档等等。
它不是安全向导。当触及一些安全相关的问题时,你要自己研究怎么样才能对你的PHP应用加固。特别的,在着手实施以前,你应该仔细回顾一下这里给出的任何建议方案。你的代码责任在于你。
它不是某种代码风格,模式或者框架的拥护者。
它不是关于如何去做高等级任务,如用户注册,登录系统等等诸如此类任务的特定方法的支持者。这篇文章完全是为低等级任务,是因为PHP的长久历史,可能会令人困惑或者不甚清楚。
它不是终极意义的解决方案,也不是唯一的方案。下面描述的一些方法可能对你实际的情况不是最优的,而且有许多能达到同样目的的不同的方法。特别的,高负载的web应用可能会从更多的针对这些问题的秘密方案中获益。
PHP如同是网络世界的百年老龟。它的外壳上刻有一个丰富的,令人费解的,粗糙的历史。在一个共享主机的环境下,它的配置可能会限制你能做什么事情。
为了保留一丝明智,我们需要只专注于一个版本的PHP。截至2013年4月30,该版本是 带有Suhosin补丁的PHP5.3.10-1ubuntu3.6 。如果你使用apt-get从一个Ubuntu12.04 LTS服务器来安装PHP的话,你所得到的版本就是这个。换句话说,许多人在默认情况下已经很明智地使用了它。
您可能会发现本文这些解决方案能工作于不同或更旧版本的PHP。如果是这样的话,就要由你来研究在这些旧版本中的细微错误或安全问题的影响了。
用 phpass 0.3 进行的测试。
散列化是在把用户密码保存进数据库之前对其进行保护的标准方法。许多常见的散列算法,如MD5,乃至SHA1,用于存储密码都是不安全的,因为黑客可以使用这些散列算法轻松破解密码。
要散列化密码最安全的方法是使用bcrypt算法。开源的phpass 库用一个易于使用的类来提供这个功能。
01 |
02 | // 包含phpass库 |
03 | require_once('phpass-0.3/PasswordHash.php'); |
04 |
05 | // 初始化散列器为不可移植(这样更安全) |
06 | $hasher = new PasswordHash(8, false); |
07 |
08 | // 计算密码哈希值。$hashedPassword 将会是一长为60个字符的字符串. |
09 | $hashedPassword = $hasher->HashPassword('my super cool password'); |
10 |
11 | // 你现在可以安全地保存$hashedPassword到数据库中! |
12 |
13 | // 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码 |
14 | $hasher->CheckPassword('the wrong password', $hashedPassword); // 返回假 |
15 |
16 | $hasher->CheckPassword('my super cool password', $hashedPassword); // 返回真 |
17 | ?> |
How to safely store a password
在PHP中有很多方法连接到一个MySQL数据库。 PDO (PHP Data Objects) 是其中最新最健壮的。对于许多不同类型的数据库,PDO都使用一致性的接口,采用面向对象的方式,并支持较新的数据库的提供的更多特性。
您应该使用PDO预定义语句的功能,以帮助防止SQL注入攻击。使用 bindValue() 函数确保你的SQL对于第一阶的SQL注入攻击是安全的(但这不是100%万无一失的,参考 进一步阅读 获得更详细说明)。在过去,这只能用一些“魔术引号”函数的复杂结合来实现。PDO使所有这些黏糊糊的东西变得不再必要了。
01 |
02 | try{ |
03 | // Create a new connection. |
04 | // You'll probably want to replace hostname with localhost in the first parameter. |
05 | // The PDO options we pass do the following: |
06 | // \PDO::ATTR_ERRMODE enables exceptions for errors. This is optional but can be handy. |
07 | // \PDO::ATTR_PERSISTENT disables persistent connections, which can cause concurrency issues in certain cases. See "Gotchas". |
08 | // \PDO::MYSQL_ATTR_INIT_COMMAND alerts the connection that we'll be passing UTF-8 data. This may not be required depending on your configuration, but it'll save you headaches down the road if you're trying to store Unicode strings in your database. See "Gotchas". |
09 | $link = new \PDO( 'mysql:host=your-hostname;dbname=your-db', |
10 | 'your-username', |
11 | 'your-password', |
12 | array( |
13 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
14 | \PDO::ATTR_PERSISTENT => false, |
15 | \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4' |
16 | ) |
17 | ); |
18 |
19 | $handle = $link->prepare('select Username from Users where UserId = ? or Username = ? limit ?'); |
20 |
21 | // PHP bug: if you don't specify PDO::PARAM_INT, PDO may enclose the argument in quotes. This can mess up some MySQL queries that don't expect integers to be quoted. |
22 | // See: https://bugs.php.net/bug.php?id=44639 |
23 | // If you're not sure whether the value you're passing is an integer, use the is_int() function. |
24 | $handle->bindValue(1, 100, PDO::PARAM_INT); |
25 | $handle->bindValue(2, 'Bilbo Baggins'); |
26 | $handle->bindValue(3, 5, PDO::PARAM_INT); |
27 |
28 | $handle->execute(); |
29 |
30 | // Using the fetchAll() method might be too resource-heavy if you're selecting a truly massive amount of rows. |
31 | // If that's the case, you can use the fetch() method and loop through each result row one by one. |
32 | // You can also return arrays and other things instead of objects. See the PDO documentation for details. |
33 | $result = $handle->fetchAll(\PDO::FETCH_OBJ); |
34 |
35 | foreach($result as $row){ |
36 | print($row->Username); |
37 | } |
38 | } |
39 | catch(\PDOException $ex){ |
40 | print($ex->getMessage()); |
41 | } |
42 | ?> |
当绑定整型变量时,如果不传递PDO::PARAM_INT参数有事可能会导致PDO对数据加引号。这会 搞坏特定的MySQL查询。查看该bug报告。
未使用 `set names utf8mb4` 作为首个查询,可能会导致Unicode数据错误地存储进数据库,这依赖于你的配置。如果你 绝对有把握你的Unicode编码数据不会出问题,那你可以不管这个。
启用持久连接可能会导致怪异的并发相关的问题。这不是一个PHP的问题,而是一个应用层面 的问题。只要你仔细考虑了后果,持久连接一般会是安全的。查看Stack Overfilow这个问题。
即使你使用了 `set names utf8mb4` ,你也得确认实际的数据库表使用的是utf8mb4字符集!
可以在单个execute()调用中执行多条SQL语句。只需使用分号分隔语句,但注意这个bug,在该文档所针对的PHP版本中还没修复。
进一步阅读
界定PHP代码块有几种不同方式: , = ?>, ?>,和 。虽然更短的方式更便于输入,但能保证在所有PHP服务器上都能工作的只有。如果你计划把你的PHP程序部署到一个你不能控制其配置的服务器上,你必须始终使用 。
如果你有足够权限控制PHP运行环境的配置,你会发现使用更短的标签自然更方便。但要记住, ?> 可能与XML声明冲突,而 则实际是ASP的风格.
无论你选择哪一种,请确保你保持一致!
Stack Overflow: Are PHP short tags acceptable to use?
PHP提供几种方式来自动载入含有还没被载入的类的文件。较老的方式是使用名为__autoload()的魔术全局函数。但你一次只能使用一个定义的 __autoload() 函数,所以当你包含一个也使用了 __autoload() 函数的库时,就会造成冲突。
解决这个问题的正确方式是把你的自动载入函数命名成一个唯一的名称,然后用 spl_autoload_register() 函数注册。这个函数允许定义多个 __autoload() 函数,这样你就不会踩到其它代码所含的 __autoload() 函数了。
01 |
02 | // 首先,定义你的自动载入的函数 |
03 | function MyAutoload($className){ |
04 | include_once($className . '.php'); |
05 | } |
06 |
07 | // 然后注册它. |
08 | spl_autoload_register('MyAutoload'); |
09 |
10 | // 试试让它工作! |
11 | // 因为我们没包含一个定义有MyClass的文件,所以自动加载器会介入并包含MyClass.php. |
12 | // 对本例来说,假定在MyClass.php文件中定义了MyClass类. |
13 | $var = new MyClass(); |
14 | ?> |
Stack Overflow: Efficient PHP auto-loading and naming strategies
关于“定义字符串时应使用单引号还是双引号”,已有很多笔墨评论了。单引号字符串不会进行解析,所以无论你在字符串放了什么,都会原样显示。双引号字符串会被解析,在里面的任何PHP变量都会被求值兑现。另外,对于转义字符(如换行符\n和制表符\t),在单引号和双引号中的差别也是同样的。
因为双引号字符串会在运行时进行解析,理论上能用单引号就用单引号,这应该可以提升性能,因为PHP不需要对单引号字符串额外进行解析。虽然对具有一定规模的应用来说这可能是真的,但对于一般的现实生活中的应用程序来说,效率差距微乎其微,它其实并不重要。因此对于一般的应用程序,你选择什么并不重要(译者注:因此更重要的是,当使用单引号字符串时,应确保它在以后绝无可能加入需要解析的成份,否则你就要在将来多麻烦一下把它改成双引号)。对于非常高负荷的应用程序,它可能有一点影响。选择使用哪种方式,取决于你应用程序的需求,但无论你选择哪种,应该保持一致。
Stack Overflow: Is there a performance benefit to single quotes vs double quotes in PHP?
传统上,在PHP中你会使用define()函数来定义常量。但根据一些意见,PHP也获得了用const关键字声明常量的能力。那么定义常量时,你应该使用哪一个呢?
答案就在于这两种方法之间微小的差异:
01 |
02 | // 来看看这两种方法如何处理名称空间 |
03 | namespace MiddleEarth\Creatures\Dwarves; |
04 | const GIMLI_ID = 1; |
05 | define('MiddleEarth\Creatures\Elves\LEGOLAS_ID', 2); |
06 |
07 | echo(\MiddleEarth\Creatures\Dwarves\GIMLI_ID); // 1 |
08 | echo(\MiddleEarth\Creatures\Elves\LEGOLAS_ID); // 2; 注意,对此常量,我们是用define()定义的,但也能识别空间。 |
09 |
10 | // 现在让我们来声明一些值是位移运算结果的常数来代表进入魔多的方式Mordor. |
11 | define('TRANSPORT_METHOD_SNEAKING', 1 |
12 | const TRANSPORT_METHOD_WALKING = 1 |
13 |
14 | // 接下来, 条件常量。 |
15 | define('HOBBITS_FRODO_ID', 1); |
16 |
17 | if($isGoingToMordor){ |
18 | define('TRANSPORT_METHOD', TRANSPORT_METHOD_SNEAKING); // OK! |
19 | const PARTY_LEADER_ID = HOBBITS_FRODO_ID // 编译错误: const 不能用于 if 块中 |
20 | } |
21 |
22 | // 最后, 类常量 |
23 | class OneRing{ |
24 | const MELTING_POINT_DEGREES = 1000000; // OK! |
25 | define('SHOW_ELVISH_DEGREES', 200); // 编译错误: 在类内不能使用define() |
26 | } |
27 | ?> |
因为define() 最终更为灵活,它是你避免头痛的选择,除非你非常需要类常量。用const可产生更易阅读的代码,但要付出灵活性的代价。
无论你用哪一种,应保持一致!
Stack Overflow: define() vs variable
在PHP的标准安装环境里,每个PHP脚本在每次它被访问时都会被编译成操作码(字节码)文件并执行。花时间对完全相同的脚本一遍又一遍进行编译,对大型网站来说,势必导致性能问题。
解决方案是操作码缓存。操作码缓存是一种可以记忆每个脚本的编译结果的系统,这样服务器不必浪费时间一遍遍编译。通常它们也很聪明,足以探测到一个脚本已经改变并对其重新编译,所以当你更新你的PHP源文件时,不必非得手动清除缓存。
有几种PHP操作码缓存系统可用,值得一提的是 eaccelerator, xcache, 和 APC. APC 是由PHP项目组官方提供支持的,是最活跃也是最容易安装的。它还提供了一个可选的类似memcached的持久的键-值存储。基于这些理由,它是你应该使用的。
通过在终端运行以下命令,可以在Ubuntu 12.04上安装APC:
sudo apt-get install php-apc
无需另外的配置。
APC也提供类似memcached的功能,对于你的脚本也是显而易见的。相对于使用memcached,最大的优点就是APC集成到了PHP内核中,因此你不需要再的你服务器中保存你的可动部分,PHP开发者主动地在它上面工作。另外一方面,APC不是一个分布式缓存;如果你需要这个特性,你必须使用memcached。
01 |
02 | // Store some values in the APC cache. We can optionally pass a time-to-live, but in this example the values will live forever until they're garbage-collected by APC. |
03 | apc_store('username-1532', 'Frodo Baggins'); |
04 | apc_store('username-958', 'Aragorn'); |
05 | apc_store('username-6389', 'Gandalf'); |
06 |
07 | // After storing these values, any PHP script can access them, no matter when it's run! |
08 | $value = apc_fetch('username-958', $success); |
09 | if($success === true) |
10 | print($value); // Aragorn |
11 |
12 | $value = apc_fetch('username-1', $success); // $success will be set to boolean false, because this key doesn't exist. |
13 | if($success !== true) // Note the !==, this checks for true boolean false, not "falsey" values like 0 or empty string. |
14 | print('Key not found'); |
15 |
16 | apc_delete('username-958'); // This key will no longer be available. |
17 | ?> |
PHP Manual: APC
一个缓存系统通常能够改进你的app的性能。Memcached是一个主流的选择,并且它兼容许多语言,包括PHP。
然而当它从一个PHP脚本访问一个Memcached服务器的时候,你有两个不同的,命名愚蠢的客户端库选择:Memcache和Memcached。它们是不同的库,但是有着近乎相同的名字,并且都用于访问一个Memcached实例。
事实证明Memcached库,是实现Memcached协议最好的方法。它包括一些Memcache库所没有的,有用的特性,看起来是最被积极开发的一款。
然而如果你不需要从一系列分布式服务器中访问一个Memcached实例,则使用APC来代替。APC由PHP项目支持,有很多和Memcached相似的功能, 附加的惊喜就是,她说一个操作码缓存,这能够提升你的PHP脚本的性能。
在你安装Memcached服务端之后,你需要安装Memcached客户端库。没有这个库,你的PHP脚本将不能和Memcached服务端通信。
通过在终端运行如下命令,你能够安装Memcached客户端库:
sudo apt-get install php5-memcached
查看the entry on opcode caches,了解更多关于使用APC作为一个Memcached替代选择。
Stack Overflow: Memcached vs APC, which one should I choose?
PHP有两种不同的方式使用正则表达式:PCRE (Perl-compatible, preg_*)函数和POSIX (POSIX extended, ereg_*)函数。
每一族函数使用轻微不同风格的正则表达式。幸运地,从PHP5.3.0开始,POSIX函数就被弃用了。因为这个,你不应当在新代码中使用POSIX函数。总是勇士PRCE函数,即是preg_*函数。
Getting started with PHP regular expressions
有多种方式来配置一个web服务器以提供PHP服务。传统(并且糟糕的)的方式是使用Apache的 mod_php。Mod_php将PHP 绑定到Apache自身,但是Apache对于该模块功能的管理工作非常糟糕。一旦遇到较大的流量, 就会遭受严重的内存问题。
后来两个新的可选项很快流行起来:mod_fastcgi 和mod_fcgid。两者均保持一定数量的PHP执行进程, Apache将请求发送到这些端口来处理PHP的执行。由于这些库限制了存活的PHP进程的数量, 从而大大减少了内存使用而没有影响性能。
一些聪明的人创建一个fastcgi的实现,专门为真正与PHP工作良好而设计,他们称之为 PHP-FPM。PHP 5.3.0之前,为安装它, 你得跨越许多障碍,但幸运的是,PHP 5.3.3的核心包含了PHP-FPM,因此在Ubuntu 12.04上安装它非常方便。
如下示例是针对Apache 2.2.22的,但PHP-FPM也能用于其他web服务器如Nginx
通过在终端中运行命令,在Ubuntu 12.04安装PHP-FPM和Apache:
sudo apt-get install apache2-mpm-worker libapache2-mod-fastcgi php5-fpm sudo a2enmod actions alias fastcgi
注意到我们必须使用apache2-mpm-worker,而不是apache2-mpm-prefork或者apache2-mpm-threaded。
下一步,我们将配置我们的Apache虚拟主机,以便路由PHP请求到PHP-FPM处理中。在你的Apache配置文件中放置如下(在Ubuntu 12.04中,默认的一个路径为/etc/apache2/sites-available/default)。
1 |
|
2 | AddHandler php5-fcgi .php |
3 | Action php5-fcgi /php5-fcgi |
4 | Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi |
5 | FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -idle-timeout 120 -pass-header Authorization |
6 |
最后,重启Apache和FPM进程:
1 | sudo service apache2 restart |
2 | sudo service php5-fpm restart |
Why mod_php is bad for performance
使用PHPMailer 5.1测试。
PHP提供一个mail()函数,看起来极度简单和容易。不幸的是,就像PHP中的很多事情,它的简单是容易误解的,用表面的值来使用它,容易导致严重的安全问题。
Email是一个协议的集合,具有比PHP更曲折痛苦的历史。满足它也就是说,在发送email的时候有太多的疑惑,就像PHPmail()函数应当给你的感觉一样。
PHPMailer 是一个受欢迎的,完善的开源库,提供安全发送mailis的一个简单接口。它为你处理好疑惑,以便你能够关注更重要的事情。
01 |
02 | // Include the PHPMailer library |
03 | require_once('phpmailer-5.1/class.phpmailer.php'); |
04 |
05 | // Passing 'true' enables exceptions. This is optional and defaults to false. |
06 | $mailer = new PHPMailer(true); |
07 |
08 | // Send a mail from Bilbo Baggins to Gandalf the Grey |
09 |
10 | // Set up to, from, and the message body. The body doesn't have to be HTML; check the PHPMailer documentation for details. |
11 | $mailer->Sender = 'bbaggins@example.com'; |
12 | $mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins'); |
13 | $mailer->SetFrom('bbaggins@example.com', 'Bilbo Baggins'); |
14 | $mailer->AddAddress('gandalf@example.com'); |
15 | $mailer->Subject = 'The finest weed in the South Farthing'; |
16 |
$mailer->MsgHTML(' You really must try it, Gandalf! -Bilbo '); |
17 |
18 | // Set up our connection information. |
19 | $mailer->IsSMTP(); |
20 | $mailer->SMTPAuth = true; |
21 | $mailer->SMTPSecure = 'ssl'; |
22 | $mailer->Port = 465; |
23 | $mailer->Host = 'my smpt host'; |
24 | $mailer->Username = 'my smtp username'; |
25 | $mailer->Password = 'my smtp password'; |
26 |
27 | // All done! |
28 | $mailer->Send(); |
29 | ?> |
你的web应用需要做的一个常见的任务。就是检查一个用户是否输入了一个有效的email地址。你将毫无疑问地在网上找到一堆复杂的表达式,它们声称可以解决此问题,但是最简单的方法就是使用PHP的内建函数filter_var(),它能够检验email地址。
1 |
2 | filter_var('sgamgee@example.com', FILTER_VALIDATE_EMAIL); // Returns "sgamgee@example.com". This is a valid email address. |
3 | filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL); // Returns boolean false! This is *not* a valid email address. |
4 | ?> |
PHP Manual: Types of filters
经HTML Purifier 4.4.0测试
在任何wbe应用中展示用户输出时,首先对其进行“净化”去除任何潜在危险的HTML是非常必要的。 一个恶意的用户可以制作某些HTML,若被你的web应用直接输出,对查看它的人来说会很危险。
虽然可以尝试使用正则表达式来净化HTML,但不要这样做。HTML是一种复杂的语言,试图 使用正则表达式来净化HTML几乎总是失败的。
你可能会找到建议你使用strip_tags() 函数的观点。虽然strip_tags()从技术上来说是安全的,但如果输入的不合法的HTML(比如, 没有结束标签),它就成了一个“愚蠢”的函数,可能会去除比你期望的更多的内容。由于非技术用户 在通信中经常使用字符,strip_tags()也就不是一个好的选择了。
如果阅读了 验证邮件地址 一节, 你也许也会考虑使用filter_var() 函数。然而filter_var()函数在遇到断行时会出现问题, 并且需要不直观的配置以接近htmlentities()函数的效果, 因此也不是一个好的选择。
对于简单需求的净化
如果你的web应用仅需要完全地转义(因此可以无害地呈现,但不是完全去除)HTML,则使用 PHP的内建htmlentities()函数。 这个函数要比HTML Purifier快得多,因为它不对HTML做任何验证—仅转义所有东西。
htmlentities()不同于类似功能的函数htmlspecialchars(), 它会编码所有适用的HTML实体,而不仅仅是一个小的子集。
01 |
02 | // 哦哦,用户的提交了一些恶意的html,我们需要将其在web 应用上显示! |
03 |
$evilHtml = ' Mua-ha-ha! Twiddling my evil mustache... '; |
04 |
05 | // 用 ENT_QUOTES 确保单双引号被转义. |
06 | // 用 UTF-8 编码,如果文件被存储为UTF-8格式. |
07 | // 见本文的 UTF-8 小节 |
08 | $safeHtml= htmlentities($evilHtml, ENT_QUOTES, 'UTF-8'); |
09 | // $safeHtml 已经被完全转移你可以放心的输出显示了! |
10 | ?> |
对于很多web应用来说,简单地转义HTML是不够的。你可能想完全去除任何HTML,或者允许 一小部分子集的HTML存在。若是如此,则使用HTML Purifier 库。
HTML Purifier是一个经过充分测试但效率比较低的库。这就是为什么如果你的需求并不复杂 就应使用htmlentities(),因为 它的效率要快得多。
HTML Purifier相比strip_tags() 是有优势的,因为它在净化HTML之前会对其校验。这意味着如果用户输入无效HTML,HTML Purifier相比strip_tags()更能保留HTML的原意。HTML Purifier高度可定制,允许你为HTML的一个子集建立白名单来允许这个HTML子集的实体存在 输出中。
但其缺点就是相当的慢,它要求一些设置,在一个共享主机的环境里可能是不可行的。其文档 通常也复杂而不易理解。以下示例是一个基本的使用配置。查看文档 阅读HTML Purifier提供的更多更高级的特性。
示例
<?php // Include the HTML Purifier libraryrequire_once('htmlpurifier-4.4.0/HTMLPurifier.auto.php');// Oh no! The user has submitted malicious HTML, and we have to display it in our web app!$evilHtml='<div onclick="xss();">Mua-ha-ha! Twiddling my evil mustache...';// Set up the HTML Purifier object with the default configuration.$purifier=newHTMLPurifier(HTMLPurifier_Config::createDefault());$safeHtml=$purifier->purify($evilHtml);// $safeHtml is now sanitized. You can output $safeHtml to your users without fear!?>
陷阱
进一步阅读
PHP中的UTF-8糟透了。原谅我的用词。
目前PHP在低层次上还不支持Unicode。有几种方式可以确保UTF-8字符串能够被正确处理, 但并不容易,需要深入到web应用的所有层面,从HTML,到SQL,到PHP。我们旨在提供一个简洁、 实用的概述。
基本的字符串操作,如串接 两个字符串、将字符串赋给变量,并不需要任何针对UTF-8的特殊东西。然而,多数 字符串函数,如strpos() 和strlen,就需要特殊的考虑。这些 函数都有一个对应的mb_*函数:例如,mb_strpos() 和mb_strlen()。这些对应的函数 统称为多字节字符串函数。这些多字节字符串 函数是专门为操作Unicode字符串而设计的。
当你操作Unicode字符串时,必须使用mb_*函数。例如,如果你使用substr() 操作一个UTF-8字符串,其结果就很可能包含一些乱码。正确的函数应该是对应的多字节函数, mb_substr()。
难的是始终记得使用mb_*函数。即使你仅一次忘了,你的Unicode字符串在接下来的处理中 就可能产生乱码。
并不是所有的字符串函数都有一个对应的mb_*。如果不存在你想要的那一个,那你就只能 自认倒霉了。
此外,在每个PHP脚本的顶部(或者在全局包含脚本的顶部)你都应使用 mb_internal_encoding 函数,如果你的脚本会输出到浏览器,那么还得紧跟其后加个mb_http_output() 函数。在每个脚本中显式地定义字符串的编码在以后能为你减少很多令人头疼的事情。
最后,许多操作字符串的PHP函数都有一个可选参数让你指定字符编码。若有该选项, 你应 始终显式地指明UTF-8编码。例如,htmlentities() 就有一个字符编码方式选项,在处理这样的字符串时应始终指定UTF-8。
如果你的PHP脚本访问MySQL,你有机会再数据库中,以非UTF-8字符串保存你的字符串,尽管你遵从了上述所有注意事项。
为了确保你的字符串以UTF-8格式,从PHP到MySQL,确保你的数据库和表单都设置为utf8mb4字符集,在争论你的数据库中任何其他查询之前,注意MySQL查询`set names utf8mb4`。对于一个例子,看看章节connecting to and querying a MySQL database。这是非常重要的。
为了完成UTF-8的支持,注意你必须使用`utf8mb4`字符集,而不是`utf8`字符集!查看Further Reading寻找原因。
使用 mb_http_output() 函数来确保你的PHP 输出给浏览器的文件编码为UTF-8。HTML 页面文件中
标签下有字符编码标签( charset tag )。
01 |
02 | // Tell PHP that we're using UTF-8 strings until the end of the script |
03 | mb_internal_encoding('UTF-8'); |
04 |
05 | // Tell PHP that we'll be outputting UTF-8 to the browser |
06 | mb_http_output('UTF-8'); |
07 |
08 | // Our UTF-8 test string |
09 | $string = 'Aš galiu valgyti stiklą ir jis manęs nežeidžia'; |
10 |
11 | // Transform the string in some way with a multibyte function |
12 | $string = mb_substr($string, 0, 10); |
13 |
14 | // Connect to a database to store the transformed string |
15 | // See the PDO example in this document for more information |
16 | // Note the `set names utf8mb4` commmand! |
17 | $link = new \PDO( 'mysql:host=your-hostname;dbname=your-db', |
18 | 'your-username', |
19 | 'your-password', |
20 | array( |
21 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, |
22 | \PDO::ATTR_PERSISTENT => false, |
23 | \PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4' |
24 | ) |
25 | ); |
26 |
27 | // Store our transformed string as UTF-8 in our database |
28 | // Assume our DB and tables are in the utf8mb4 character set and collation |
29 | $handle = $link->prepare('insert into Sentences (Id, Body) values (?, ?)'); |
30 | $handle->bindValue(1, 1, PDO::PARAM_INT); |
31 | $handle->bindValue(2, $string); |
32 | $handle->execute(); |
33 |
34 | // Retrieve the string we just stored to prove it was stored correctly |