PHP 易混淆知识点的实用指南(上)
原文链接:http://phpbestpractices.justjavac.com/
这是一份指南,在 PHP 程序员遇到一些常见低层次任务但不明确最佳做法(由于 PHP 可能提供了多种解决方案)之时,为其建议最佳实践。 例如:连接数据库是一个常见任务,PHP 中提供了大量可行的方案,但并不是所有的都是好的做法,因此,本文也会包含该问题。
本文包含的是一系列简短的、入门性质的方案。 涉及的示例在基本设定下就能够运行起来,你研究一下应该就能把它们变为对你有用的东西。
本文将指出一些我们认为是 PHP 中最新最好的东西。然而,这意味如果你在使用老版本的 PHP, 一些用来实现这些解决方案的特性对你并不可用。
我们在使用哪个版本的 PHP?
带 Suhosin-Patch 的 PHP 5.3.10-1ubuntu3.6,安装在 Ubuntu 12.04 LTS 上。
PHP 是 Web 世界里的百年老龟,它的壳上铭刻着一段丰富、复杂、而粗糙的历史。 在一个共享主机的环境里,它的配置可能会限制你能做的事情。
为了保持清晰地叙述,我们将仅针对一个版本的 PHP 进行讲述。 在 2013 年 4 月 30 日时,该版本为 PHP 5.3.10-1ubuntu3.6 with Suhosin-Patch。 若你在 Ubuntu 12.04 LTS 服务器上使用 apt-get 进行安装的就是该版本的 PHP。
你也许发现这些方案中的一些在其他或者更老版本的 PHP 上也能工作。 如果是这样的话,就 由你来研究在这些更老版本上潜在的难以捉摸的 bug 或安全问题。
存储密码
使用 phpass 库来哈希和比较密码
经 phpass 0.3 测试,在存入数据库之前进行哈希保护用户密码的标准方式。 许多常用的哈希算法如 md5,甚至是 sha1 对于密码存储都是不安全的, 因为骇客能够使用那些算法轻而易举地**密码。
对密码进行哈希最安全的方法是使用 bcrypt 算法。开源的 phpass 库以一个易于使用的类来提供该功能。
示例
陷阱
许多资源可能推荐你在哈希之前对你的密码“加盐”。想法很好,但 phpass 在 HashPassword() 函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。
PHP 与 MySQL
使用 PDO 及其预处理语句功能。
在 PHP 中,有很多方式来连接到一个 MySQL 数据库。PDO(PHP 数据对象)是其中最新且最健壮的一种。 PDO 跨多种不同类型数据库有一个一致的接口,使用面向对象的方式,支持更多的新数据库支持的特性。
你应该使用 PDO 的预处理语句函数来帮助防范 SQL 注入攻击。 使用函数 bindValue 来确保你的 SQL 免于一级 SQL 注入攻击。 (虽然并不是 100% 安全的,查看进一步阅读获取更多细节。) 在以前,这必须使用一些「魔术引号(magic quotes)」函数的组合来实现。PDO 使得那堆东西不再需要。
示例
陷阱
-
当绑定整型变量时,如果不传递 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 ?>, <?= ?>, <? ?>, 以及<% %>。 对于打字来说,更短的标签更方便些,但唯一一种在所有 PHP 服务器上都一定能工作的标签是<?php ?>。 若你计划将你的 PHP 应用部署到一台上面的 PHP 配置你无法控制的服务器上,那么你应始终使用 <?php ?>。
若你仅仅是为自己编码,也能控制你将使用的 PHP 配置,你可能觉得短标签更方便些。 但记住 <? ?>可能会和 XML 声明冲突,并且<? ?>实际上是 ASP 的风格。
无论你选择哪一种,确保一致。
陷阱
-
在一个纯 PHP 文件(例如,仅包含一个类定义的文件)中包含一个关闭?>标签时,确保其后不会跟着任何换行。 当 PHP 解析器安全地吃进跟在关闭标签之后的单个换行符时,任何其他的换行都可能被输出到浏览器,如果之后要输出某些 HTTP 头,那么可能会造成混淆。
编写Web应用时,确保在关闭?>标签与 html 的<!doctype>标签之间不会留下换行。 正确的 HTML 文件中,<!doctype>标签必须是文件中的第一样东西---在其之前的任何空格或换行都会使其无效。
自动加载类
使用 spl_autoload_register() 来注册你的自动加载函数。
PHP 提供了若干方式来自动加载包含还未加载的类的文件。 老的方法是使用名为 __autoload() 魔术全局函数。 然而你一次仅能定义一个 __autoload() 函数,因此如果你的程序包含一个也使用了 __autoload() 函数的库,就会发生冲突。
处理这个问题的正确方法是唯一地命名你的自动加载函数,然后使用 spl_autoload_register() 函数来注册它。 该函数允许定义多个 __autoload() 这样的函数,因此你不必担心其他代码的 __autoload() 函数。
示例
从性能角度来看单引号和双引号
其实并不重要。
已有很多人花费很多笔墨来讨论是使用单引号(')还是双引号(")来定义字符串。 单引号字符串不会被解析,因此放入字符串的任何东西都会以原样显示。 双引号字符串会被解析,字符串中的任何 PHP 变量都会被求值。 另外,转义字符如换行符 \n 和制表符 \t 在单引号字符串中不会被求值,但在双引号字符串中会被求值。
由于双引号字符串在程序运行时要求值,从而理论上使用单引号字符串能提高性能,因为 PHP 不会对单引号字符串求值。 这对于一定规模的应用来说也许确实如此,但对于现实中一般的应用来说, 区别非常小以至于根本不用在意。因此对于普通应用,你选择哪种字符串并不重要。 对于负载极其高的应用来说,是有点作用的。 根据你的应用的需要来做选择,但无论你选择什么,请保持一致。
define() vs. const
使用 define(),除非考虑到可读性、类常量、或关注微优化。
习惯上,在 PHP 中是使用 define() 函数来定义常量。 但从某个时候开始,PHP 中也能够使用 const 关键字来声明常量了。 那么当定义常量时,该使用哪种方式呢?
答案在于这两种方法之间的区别。
-
define() 在执行期定义常量,而 const 在编译期定义常量。这样 const 就有轻微的速度优势, 但不值得考虑这个问题,除非你在构建大规模的软件。
-
define() 将常量放入全局作用域,虽然你可以在常量名中包含命名空间。 这意味着你不能使用 define() 定义类常量。
-
define() 允许你在常量名和常量值中使用表达式,而 const 则都不允许。 这使得 define() 更加灵活。
define() 可以在 if() 代码块中调用,但 const 不行。
示例
小插曲:当我看到第一行的 MiddleEarth 还没有感觉到什么,再往下看到 Mordor 时,震惊了。OneRing,OneRing,OneRingggggg!
因为 define() 更加灵活,你应该使用它以避免一些令人头疼的事情,除非你明确地需要类常量。 使用 const 通常会产生更加可读的代码,但是以牺牲灵活性为代价的。
无论你选择哪一种,请保持一致。
缓存 PHP opcode
使用 APC
在一个标准的 PHP 环境中,每次访问PHP脚本时,脚本都会被编译然后执行。 一次又一次地花费时间编译相同的脚本对于大型站点会造成性能问题。
解决方案是采用一个 opcode 缓存。 opcode 缓存是一个能够记下每个脚本经过编译的版本,这样服务器就不需要浪费时间一次又一次地编译了。 通常这些 opcode 缓存系统也能智能地检测到一个脚本是否发生改变,因此当你升级 PHP 源码时,并不需要手动清空缓存。
PHP 5.5 内建了一个缓存 OPcache。PHP 5.2 - 5.4 下可以作为 PECL 扩展安装。
此外还有几个PHP opcode 缓存值得关注 eaccelerator, xcache,以及APC。 APC 是 PHP 项目官方支持的,最为活跃,也最容易安装。 它也提供一个可选的类 memcached 的持久化键-值对存储,因此你应使用它。
安装 APC
在 Ubuntu 12.04 上你可以通过在终端中执行以下命令来安装 APC:
[email protected]: sudo apt-get install php-apc
除此之外,不需要进一步的配置。
将 APC 作为一个持久化键-值存储系统来使用
APC 也提供了对于你的脚本透明的类似于 memcached 的功能。 与使用 memcached 相比一个大的优势是 APC 是集成到 PHP 核心的,因此你不需要在服务器上维护另一个运行的部件, 并且 PHP 开发者在 APC 上的工作很活跃。 但从另一方面来说,APC 并不是一个分布式缓存,如果你需要这个特性,你就必须使用 memcached 了。
示例
陷阱
如果你使用的不是 PHP-FPM(例如你在使用 mod_php 或 mod_fastcgi), 那么每个 PHP 进程都会有自己独有的 APC 实例,包括键-值存储。 若你不注意,这可能会在你的应用代码中造成同步问题。
PHP 与 Memcached
若你需要一个分布式缓存,那就使用 Memcached 客户端库。否则,使用 APC。
缓存系统通常能够提升应用的性能。Memcached 是一个受欢迎的选择,它能配合许多语言使用,包括 PHP。
然而,从一个 PHP 脚本中访问一个 Memcached 服务器,你有两个不同且命名很愚蠢的客户端库选择项: Memcache 和 Memcached。 它们是两个名字几乎相同的不同库,两者都可用于访问一个 Memcached 实例。
事实证明,Memcached 库对于 Memcached 协议的实现最好,包含了一些 Mmecache 库没有的有用的特性, 并且看起来 Memcached 库的开发也最为活跃。
然而,如果不需要访问来自一组分布式服务器的一个 Memcached 实例,那就使用 APC。 APC 得到 PHP 项目的支持,具备很多和 Memcached 相同的功能,并且能够用作 opcode 缓存,这能提高 PHP 脚本的性能。
安装 M emcached 客户端库
在安装 Memcached 服务器之后,需要安装 Memcached 客户端库。没有该库,PHP 脚本就没法与 Memcached 服务器通信。
在 Ubuntu 12.04 上,你可以使用如下命令来安装 Memcached 客户端库:
[email protected]: sudo apt-get install php5-memcached
使用 APC 作为替代
查看 opcode 缓存一节阅读更多与使用 APC 作为 Memcached 替代方案相关的信息。
PHP 与正则表达式
使用 PCRE(preg_*) 家族函数
PHP有两种使用不同的方式来使用正则表达式:PCRE(Perl兼容表示法,preg_*)函数 和 POSIX(POSIX 扩展表示法,ereg_*) 函数。
每个函数家族各自使用一种风格稍微不同的正则表达式。幸运的是,POSIX 家族函数从 PHP 5.3.0 开始就被弃用了。因此,你绝不应该使用 POSIX 家族函数编写新的代码。 始终使用 PRCE 家族函数,即 preg_*函数。
配置 Web 服务器提供 PHP 服务
使用 PHP-FPM
有多种方式来配置一个 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。
安装 PHP-FPM 和 Apache
在 Ubuntu 12.04 上你可以使用如下命令安装 PHP-FPM 和 Apache:
[email protected]: sudo apt-get install apache2-mpm-worker
libapache2-mod-fastcgi php5-fpm
[email protected]: sudo a2enmod actions alias fastcgi
注意我们 必须 使用 apache2-mpm-worker,而不是 apache2-mpm-prefork 或 apache2-mpm-threaded。
接下来配置 Aapache 虚拟主机将 PHP 请求路由到 PHP-FPM 进程。将如下配置语句放入 Apache 配置文件(在 Ubuntu 12.04 上默认配置文件是 /etc/apache2/sites-available/default)。
<VirtualHost *:80>
AddHandler php5-fcgi .php
Action php5-fcgi /php5-fcgi
Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi
FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -idle-timeout 120 -pass-header Authorization
</VirtualHost>
最后,重启 Apache 和 FPM 进程:
[email protected]: sudo service apache2 restart && sudo service php5-fpm
restart
发送邮件
使用 PHPMailer
经 PHPMailer 5.1 测试
PHP 提供了一个 mail() 函数,看起来很简单易用。 不幸的是,与 PHP 中的很多东西一样,它的简单性是个幻象,因其虚假的表面使用它会导致严重的安全问题。
Email 是一组网络协议,比 PHP 的历史还曲折。完全可以说发送邮件中的陷阱与 PHP 的 mail() 函数一样多,这个可能会令你有点「不寒而栗」吧。
PHPMailer 是一个流行而成熟的开源库,为安全地发送邮件提供一个易用的接口。 它关注可能陷阱,这样你可以专注于更重要的事情。
示例
验证邮件地址
使用 filter_var() 函数
Web 应用可能需要做的一件常见任务是检测用户是否输入了一个有效的邮件地址。毫无疑问你可以在网上找到一些声称可以解决该问题的复杂的正则表达式,但是最简单的方法是使用 PHP 的内建 filter_val() 函数。
示例
注意
邮件地址验证也可以交给前端解决。HTML 5 的 表单即支持验证邮箱地址。只需将input元素的type设为email,就会自动验证用户输入的是否是合法的邮件地址。
<input type="email" name="email"></pre>
(未完待续...)
end
LeanCloud,领先的 BaaS 提供商,为移动开发提供强有力的后端支持。更多内容请关注「 LeanCloud 通讯」