存储密码, 使用 phpass
^1 库来哈希和比较密码
// Include phpass 库
require_once('phpass-03/PasswordHash.php')
// 初始化散列器为不可移植(这样更安全)
$hasher = new PasswordHash(8, false);
// 计算密码的哈希值。$hashedPassword 是一个长度为 60 个字符的字符串.
$hashedPassword = $hasher->HashPassword('my super cool password');
// 你现在可以安全地将 $hashedPassword 保存到数据库中!
// 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码
$hasher->CheckPassword('the wrong password', $hashedPassword); // false
$hasher->CheckPassword('my super cool password', $hashedPassword); // true
phpass 在 HashPassword() 函数中已经对你的密码“加盐”了,这意味着你不需要自己“加盐”。
PHP 与 MySQL, 使用 PDO
及其预处理语句功能。
try{
// 新建一个数据库连接
// You'll probably want to replace hostname with localhost in the first parameter.
// The PDO options we pass do the following:
// \PDO::ATTR_ERRMODE enables exceptions for errors. This is optional but can be handy.
// \PDO::ATTR_PERSISTENT disables persistent connections, which can cause concurrency issues in certain cases. See "Gotchas".
// \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".
$link = new \PDO( 'mysql:host=your-hostname;dbname=your-db',
'your-username',
'your-password',
array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_PERSISTENT => false,
\PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
)
);
$handle = $link->prepare('select Username from Users where UserId = ? or Username = ? limit ?');
// 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.
// See: https://bugs.php.net/bug.php?id=44639
// If you're not sure whether the value you're passing is an integer, use the is_int() function.
$handle->bindValue(1, 100, PDO::PARAM_INT);
$handle->bindValue(2, 'Bilbo Baggins');
$handle->bindValue(3, 5, PDO::PARAM_INT);
$handle->execute();
// Using the fetchAll() method might be too resource-heavy if you're selecting a truly massive amount of rows.
// If that's the case, you can use the fetch() method and loop through each result row one by one.
// You can also return arrays and other things instead of objects. See the PDO documentation for details.
$result = $handle->fetchAll(\PDO::FETCH_OBJ);
foreach($result as $row){
print($row->Username);
}
}
catch(\PDOException $ex){
print($ex->getMessage());
}
- 当绑定整型变量时,如果不传递 PDO::PARAM_INT 参数有事可能会导致 PDO 对数据加引号。 这会搞坏特定的 MySQL 查询。
- 未使用
set names utf8mb4
作为首个查询,可能会导致 Unicode 数据错误地存储进数据库 - 即使你使用了
set names utf8mb4
,你也得确认实际的数据库表使用的是 utf8mb4 字符集! - 可以在单个 execute() 调用中执行多条 SQL 语句。 只需使用分号分隔语句, 但是 PDO::nextRowset() after a multi-statement query doesn’t always work ^2
自动加载类, 使用 spl_autoload_register()
来注册你的自动加载函数。
<?php
// 首先,定义你的自动载入的函数
function MyAutoload($className){
include_once($className . '.php');
}
// 然后注册它。
spl_autoload_register('MyAutoload');
// Try it out!
// 因为我们没包含一个定义有 MyClass 的文件,所以自动加载器会介入并包含 MyClass.php。
// 在本例中,假定在 MyClass.php 文件中定义了 MyClass 类。
$var = new MyClass();
从性能角度来看单引号和双引号, 其实并不重要。
单引号字符串不会被解析,因此放入字符串的任何东西都会以原样显示。 双引号字符串会被解析,字符串中的任何 PHP 变量都会被求值。
define() vs.
const
使用
define()
,除非考虑到可读性、类常量、或关注微优化
- define() 在执行期定义常量,而 const 在编译期定义常量。这样 const 就有轻微的速度优势, 但不值得考虑这个问题,除非你在构建大规模的软件。
- define() 将常量放入全局作用域. 不能使用 define() 定义类常量。
- define() 可以在 if() 代码块中调用,但 const 不行。
缓存 PHP opcode, 使用 APC
<?php
// 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.
apc_store('username-1532', 'Frodo Baggins');
apc_store('username-958', 'Aragorn');
apc_store('username-6389', 'Gandalf');
// After storing these values, any PHP script can access them, no matter when it's run!
$value = apc_fetch('username-958', $success);
if($success === true)
print($value); // Aragorn
$value = apc_fetch('username-1', $success); // $success will be set to boolean false, because this key doesn't exist.
if($success !== true) // Note the !==, this checks for true boolean false, not "falsey" values like 0 or empty string.
print('Key not found');
apc_delete('username-958'); // This key will no longer be available.
配置 Web 服务器提供 PHP 服务, 使用 PHP-FPM
发送邮件, 使用 PHPMailer ^3
<?php
// Include the PHPMailer library
require_once('phpmailer-5.1/class.phpmailer.php');
// Passing 'true' enables exceptions. This is optional and defaults to false.
$mailer = new PHPMailer(true);
// Send a mail from Bilbo Baggins to Gandalf the Grey
// Set up to, from, and the message body. The body doesn't have to be HTML;
// check the PHPMailer documentation for details.
$mailer->Sender = 'bbaggins@example.com';
$mailer->AddReplyTo('bbaggins@example.com', 'Bilbo Baggins');
$mailer->SetFrom('bbaggins@example.com', 'Bilbo Baggins');
$mailer->AddAddress('gandalf@example.com');
$mailer->Subject = 'The finest weed in the South Farthing';
$mailer->MsgHTML('<p>You really must try it, Gandalf!</p><p>-Bilbo</p>');
// Set up our connection information.
$mailer->IsSMTP();
$mailer->SMTPAuth = true;
$mailer->SMTPSecure = 'ssl';
$mailer->Port = 465;
$mailer->Host = 'my smpt host';
$mailer->Username = 'my smtp username';
$mailer->Password = 'my smtp password';
// All done!
$mailer->Send();
验证邮件地址, 使用 filter_var()
函数 ^4
<?php
filter_var('sgamgee@example.com', FILTER_VALIDATE_EMAIL);
//Returns "sgamgee@example.com". This is a valid email address.
filter_var('sauron@mordor', FILTER_VALIDATE_EMAIL);
// Returns boolean false! This is *not* a valid email address.
净化 HTML 输入和输出
- 对于简单的数据净化,使用
htmlentities()
函数, 复杂的数据净化则使用 HTML Purifier 库 - 你可能会找到建议你使用 strip_tags() 函数的观点。 虽然 strip_tags() 从技术上来说是安全的,但如果输入的不合法的 HTML(比如, 没有结束标签),它就成了一个「愚蠢」的函数,可能会去除比你期望的更多的内容。
- 如果你的 web 应用仅需要完全地转义(因此可以无害地呈现,但不是完全去除) HTML, 则使用 PHP 的内建 htmlentities() 函数。
<?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...</div>';
// Use the ENT_QUOTES flag to make sure both single and double quotes are escaped.
// Use the UTF-8 character encoding if you've stored the text as UTF-8 (as you should have).
// See the UTF-8 section in this document for more details.
$safeHtml = htmlentities($evilHtml, ENT_QUOTES, 'UTF-8');
// $safeHtml is now fully escaped HTML. You can output $safeHtml to your users without fear!
对于很多 web 应用来说,简单地转义 HTML 是不够的。 你可能想完全去除任何HTML,或者允许一小部分子集的 HTML 存在。 若是如此,则使用 HTML Purifier 库。
<?php
// Include the HTML Purifier library
require_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...</div>';
// Set up the HTML Purifier object with the default configuration.
$purifier = new HTMLPurifier(HTMLPurifier_Config::createDefault());
$safeHtml = $purifier->purify($evilHtml);
// $safeHtml is now sanitized. You can output $safeHtml to your users without fear!
HTML Purifier 相比 strip_tags() 是有优势的, 因为它在净化 HTML 之前会对其校验。这意味着如果用户输入无效 HTML,HTML Purifier 相比 strip_tags() 更能保留 HTML 的原意。
- 以错误的字符编码使用
htmlentities()
会造成意想不到的输出。 - 使用 htmlentities() 时,始终包含 ENT_QUOTES 和字符编码参数。 默认情况下,htmlentities() 不会 对单引号编码。
- HTML Purifier 对于复杂的 HTML 效率极其的低。可以考虑设置一个缓存方案如APC来保存经过净化的结果以备后用。
PHP 与 UTF-8
基本的字符串操作,如串接 两个字符串、将字符串赋给变量,并不需要任何针对 UTF-8 的特殊东西。多数 字符串函数,如
strpos()
和 strlen,就需要特殊的考虑。 这些函数都有一个对应的mb_*
函数:例如,mb_strpos() 和 mb_strlen()。 这些对应的函数统称为多字节字符串函数。 这些多字节字符串函数是专门为操作 Unicode 字符串而设计的。
- 并不是所有的字符串函数都有一个对应的 mb_*。
- 在每个 PHP 脚本的顶部(或者在全局包含脚本的顶部)你都应使用
mb_internal_encoding
函数,如果你的脚本会输出到浏览器,那么还得紧跟其后加个mb_http_output()
函数。 - 许多操作字符串的 PHP 函数都有一个可选参数让你指定字符编码。 若有该选项, 你应始终显式地指明 UTF-8 编码。 例如,htmlentities() 就有一个字符编码方式选项,在处理这样的字符串时应始终指定 UTF-8。
确保从 PHP 到 MySQL 的字符串为 UTF-8 编码的,确保你的数据库以及数据表均设置为 utf8mb4 字符集, 并且在你的数据库中执行任何其他查询之前先执行 MySQL 查询
set names utf8mb4
。
- 注意你必须使用
utf8mb4
字符集来获得完整的 UTF-8 支持,而不是utf8
字符集!
使用 mb_http_output() 函数 来确保你的 PHP 脚本输出 UTF-8 字符串到浏览器。 并且在 HTML 页面的
标签块中包含 字符集 标签块。
<?php
// Tell PHP that we're using UTF-8 strings until the end of the script
mb_internal_encoding('UTF-8');
// Tell PHP that we'll be outputting UTF-8 to the browser
mb_http_output('UTF-8');
// Our UTF-8 test string
$string = 'Aš galiu valgyti stiklą ir jis manęs nežeidžia';
// Transform the string in some way with a multibyte function
$string = mb_substr($string, 0, 10);
// Connect to a database to store the transformed string
// See the PDO example in this document for more information
// Note the `set names utf8mb4` commmand!
$link = new \PDO( 'mysql:host=your-hostname;dbname=your-db',
'your-username',
'your-password',
array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_PERSISTENT => false,
\PDO::MYSQL_ATTR_INIT_COMMAND => 'set names utf8mb4'
)
);
// Store our transformed string as UTF-8 in our database
// Assume our DB and tables are in the utf8mb4 character set and collation
$handle = $link->prepare('insert into Sentences (Id, Body) values (?, ?)');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->bindValue(2, $string);
$handle->execute();
// Retrieve the string we just stored to prove it was stored correctly
$handle = $link->prepare('select * from Sentences where Id = ?');
$handle->bindValue(1, 1, PDO::PARAM_INT);
$handle->execute();
// Store the result into an object that we'll output later in our HTML
$result = $handle->fetchAll(\PDO::FETCH_OBJ);
?><!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>UTF-8 test page</title>
</head>
<body>
<?php
foreach($result as $row){
print($row->Body); // This should correctly output our transformed UTF-8 string to the browser
}
?>
</body>
</html>
处理日期和时间, 使用DateTime
类。
<?php
// Construct a new UTC date. Always specify UTC unless you really know what you're doing!
$date = new DateTime('2011-05-04 05:00:00', new DateTimeZone('UTC'));
// Add ten days to our initial date
$date->add(new DateInterval('P10D'));
echo($date->format('Y-m-d h:i:s')); // 2011-05-14 05:00:00
// Sadly we don't have a Middle Earth timezone
// Convert our UTC date to the PST (or PDT, depending) time zone
$date->setTimezone(new DateTimeZone('America/Los_Angeles'));
// Note that if you run this line yourself, it might differ by an hour depending on daylight savings
echo($date->format('Y-m-d h:i:s')); // 2011-05-13 10:00:00
$later = new DateTime('2012-05-20', new DateTimeZone('UTC'));
// Compare two dates
if($date < $later)
echo('Yup, you can compare dates using these easy operators!');
// Find the difference between two dates
$difference = $date->diff($later);
echo('The 2nd date is ' . $difference['days'] . ' later than 1st date.');
- 如果你不指定一个时区,
DateTime::__construct()
就会将生成日期的时区设置为正在运行的计算机的时区。在创建新日期时始终指定 UTC 时区,除非你确实清楚自己在做的事情。 - 如果你在
DateTime::__construct()
中使用 Unix 时间戳,那么时区将始终设置为 UTC 而不管第二个参数你指定了什么。 - 向
DateTime::__construct()
传递零值日期(如:“0000-00-00”,常见 MySQL 生成该值作为 DateTime 类型数据列的默认值)会产生一个无意义的日期,而不是“0000-00-00”。 - 在 32 位系统上使用 DateTime::getTimestamp() 不会产生代表 2038 年之后日期的时间戳。64 位系统则没有问题。
检测一个值是否为 null 或 false
使用 === 操作符来检测 null 和布尔 false 值。
is_null() 函数能准确地检测一个值是否为 null, is_bool 可以检测一个值是否是布尔值(比如 false), 但存在一个更好的选择:=== 操作符。
原文: PHP 最佳实践(译)
参考文档
- How To Safely Store A Password
- Why you Should be using PHP’s PDO for Database Access
- 深入理解PHP原理之Opcodes
- INSTALLING APACHE + MOD_FASTCGI + PHP-FPM ON UBUNTU SERVER MAVERICK
- FILTER_SANITIZE_SPECIAL_CHARS problem with line breaks
- PCRE 正则语法 递归模式
- Getting Started with PHP Regular Expressions
- What factors make PHP Unicode-incompatible?
- PHP UTF-8 cheatsheet
- What’s the best method for sanitizing user input with PHP?
- Portable PHP password hashing framework
- Bug #61207 PDO::nextRowset() after a multi-statement query doesn’t always work
- PHPMailer/PHPMailer
- Filter > Types of filters