过滤输入:
是指转义或者删除不安全的字符。在数据到达应用的储存层(mysql or redis)之前,一定要过滤输入的数据。
过滤HTML数据:
使用htmlentities()函数。默认情况下,这个函数不会转义单引号。而且也检测不出输入字符串的字符集。htmlentities()函数的正确使用方式是:第一个参数是输入字符串;第二个参数设定为ENT_QUOTES常量,转义单引号;第三个参数设定为输入字符串的字符集。
Example:
$input = '
';echo htmlentities($input, ENT_QUOTES, 'UTF-8');
SQL查询:
有时必须根据输入数据构建SQL查询。直接拼接$_GET或者$_POST中的原始输入数据会对数据库造成巨大伤害。
要使用PDO预处理语句(prepared statement)。
$sql = 'SELECT id FROM users WHERE email = :email';
$statement = $pdo->prepare($sql);
$email = filter_input(INPUT_GET, 'email');
$statement->bindValue(':email', $email);
预处理语句会自动过滤$email的值,防止数据库收到SQL注入攻击。一个SQL语句字符串中可以有很多个具名占位符,然后在预处理语句上调用bindValue()方法绑定各个占位符的值。
用户资料信息:
应用中如果有用户资料信息,可能就需要处理邮件地址,电话,邮政编码等资料信息。这种情况,则需要使用filter_var()和filter_input()函数。这两个函数可以有效的过滤不同类型的输入:电子邮件地址,URL编码字符串,整数等。
Example:
$email = "[email protected]";
$safeEmail = filter_var($email, FILTER_SANITIZE_EMAIL);
验证数据:
在过滤数据之后,也需要验证数据:
Example:
$email = "[email protected]";
$isEmail = filter_var($email, FILTER_VALIDATE_EMAIL);
if ($isEmail !== false) {
echo "Success";
} else {
echo "Fail";
}
转义输出:
把输出渲染成网页或者API响应时,一定要转义输出。这也是一种防护措施,能避免渲染恶意代码,还能防止应用的用户无意之中执行恶意代码。
htmlentities函数可以转义输出,另外一些PHP模板引擎也能如smarty/smarty,会自动转义输出。
使用bcrypt计算用户密码的哈希值:
我们应该计算用户密码的哈希值,而不能加密用户的密码。加密和哈希不是一回事。加密是双向算法,加密的数据以后可以解密。而哈希是单向算法,哈希后的数据不能再还原成原始值,而且相同的数据得到的哈希值始终相同。
数据库中的密码需要储存成哈希值而不是明文。
最安全的哈希算法是bcrypt。
注册用户例子:
try {
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if (!$email) {
throw new Exception("Invalid Email");
}
$password = filter_input(INPUT_POST, 'password');
if (!$password || mb_strlen($password) < 8) {
throw new Exception("Password must contain 8+ characters");
}
$passwordHash = password_hash(
$password,
PASSWORD_DEFAULT,
['cost' => 12]
);
if ($passwordHash === false) {
throw new Exception("Password hash failed");
}
$user = new User();
$user->email = $email;
$user->password_hash = $passwordHash;
$user->save();
header('HTTP/1.1 302 Redirect');
header('Location: /login.php');
} catch (Exception $e) {
header('HTTP/1.1 400 Bad request');
echo $e->getMessage();
}
?>
注意:密码的哈希值要储存再VARCHAR(255)类型的数据库中。这样便于以后存储比现在的bcrypt算法得到的哈希值更长的密码。
Login的示例:
session_start();
try {
$email = filter_input(INPUT_POST, 'email');
$password = filter_input(INPUT_POST, 'password');
$user = User::findByEmail($email);
if (password_verify($password, $user->password_hash) === false) {
throw new Exception("Invalid Password", 1);
}
$currentHashAlgorithm = PASSWORD_DEFAULT;
$currentHashOptions = array('cost' => 15);
$passwordNeedsRehash = password_needs_rehash(
$user->password_hash,
$currentHashAlgorithm,
$currentHashOptions
);
if ($passwordNeedsRehash === true) {
$user->password_hash = password_hash(
$password,
$currentHashAlgorithm,
$currentHashOptions
);
$user->save();
}
$_SESSION['user_logged_in'] = 'yes';
$_SESSION['user_email'] = $email;
header('HTTP/1.1 302 Redirect');
header('Location: /user-profile.php');
} catch (Exception $e) {
header('HTTP/1.1 401 Unauthorized');
echo $e->getMessage();
}
?>
password_verify()函数需要接受两个参数,第一个是纯文本密码,第二个是用户记录中现有的密码哈希值。如果函数返回的是true,则证明纯文本密码正确。
password_needs_rehash()函数检查用户记录中现有的密码哈希值是否需要更新。这个函数能确保指定的密码哈希值是使用的最新的哈希算法选项创建的。
密码的哈希值需要更新是因为哈希值的作用因子会增长(因为计算机越来越快,作用因子不够多会影响安全性)
日期,时间和时区:
首先要为PHP中处理日期和时间的函数设置默认时区。可以放在php.ini文件中:
date.timezone = 'America/New_York';
也可以在运行时使用date_default_timezone_set()函数设置默认时区。
PDO扩展:
PHP Data Objects (数据对象)是一系列PHP的类,抽象了不同的数据库具体实现。
当我们使用数据库的时候,需要保证数据库凭据的安全。应该把数据库凭据保存在一个位于文档根目录之外的配置文件中,然后在需要使用凭据时导入。千万不能把凭据保存到公共的库里,如github。
事务:
PDO扩展还支持事务。事务是指把一系列数据库语句当成个逻辑单元(具有原子性)执行。也就是说,事务中的一系列SQL查询要么都成功执行,要么根本不执行。事务的原子性能保证数据的一致性,安全性和持久性。事务还有个很好的副作用----提升性能,因为事务其实是把多个查询排成队列,一次性全部执行。
Example:
require 'settings.php';
// PDO Connection
try {
$pdo = new PDO(
sprintf(
'mysql:host=%s;dbname=%s;port=%s;charset=%s',
$settings['host'],
$settings['name'],
$settings['port'],
$settings['charset'],
),
$settings['username'],
$settings['password']
);
} catch (PDOException $e) {
// Failed to connect
echo "Database connection failed";
exit;
}
// Query
$stmtSubstract = $pdo->prepare('
UPDATE accounts
SET amount = amount - :amount
WHERE name = :name
');
$stmtAdd = $pdo->prepare('
UPDATE accounts
SET amount = amount + :amount
WHERE name = :name
');
// Start the transaction
$pdo->beginTransaction();
// Withdrawal money from the account
$fromAccount = 'Checking';
$withdrawal = 50;
$stmtSubstract->bindParam(':name', $fromAccount);
$stmtSubstract->bindParam(':amount', $withdrawal, PDO::PARAM_INT);
$stmtSubstract->execute();
// Save money to account 2
$toAccount = 'Savings';
$deposit = 50;
$stmtAdd->bindParam(':name', $toAccount);
$stmtAdd->bindParam(':amount', $deposit, PDO::PARAM_INT);
$stmtAdd->execute();
// submit transaction
$pdo->commit();
?>
多字节字符串:
PHP假设字符串中每个字符都是八位字符,占一个内存,可是很多语言使用的字符串会多于8位字符串。mbstring扩展提供了处理多字节字符串,能代替大多数PHP原生的处理字符串的函数。如,mb_strlen()函数用于替代PHP原生的strlen()函数。
字符编码:
所有的现代的WEB浏览器都能处理UTF-8字符编码。字符编码是打包Unicode数据方式,以便把数据存储在内存中,或者通过线缆在服务器和客户端之间传输。
1. 一定要知道数据的字符编码。
2. 使用UTF-8字符编码存储数据。
3. 使用UTF-8字符编码输出数据。
mbstring扩展不仅能处理Unicode字符串,还能再不同的字符编码之间转换多字节字符串。如果客户使用Windows专用的字符编码到处Excel电子表格,我们可以通过mb_detect_encoding()和mb_convert_encoding()函数可以把Unicode字符串从一种字符串编码成另一种字符编码。
流:
流的作用是在出发地和目的地之间传输数据,流为PHP的很多IO函数提供底层实现,如file_get_contents(), fopen(), fgets()等。
流的封装协议:
我们读写文件可以通过HTTP, HTTPS或者SSH与远程服务器通信,还可以打开读写ZIP,RAR,等压缩文件。
1. 开始通信
2. 读取数据
3. 写入数据
4. 结束通信
每个流都有一个协议和一个目标。指定协议和目标的方法是使用流标识符,其格式如下:
其中,
$json = file_get_contents(
'http://api.flickr.com/services/feeds/photo_public.gne?format=json'
);
这其中“http”协议会让PHP使用http流封装协议。http之后是流的目标。这就是HTTP流封装协议所规定的。
file://也是流封装协议,不过我们通常可以省略因为这是PHP默认的封装协议
$handle = fopen('/etc/hosts', 'rb'); // 我们省略了file://流封装协议
$handle = fopen('file://etc/hosts', 'rb'); // 我们标注了file://流封装协议。
fopen(), fgets(), fputs()等文件系统函数可以处理ZIP压缩文件以及Amazon S3服务。
我们也可以自定义流,用来支持部分或全部PHP文件系统函数。
流过滤器:
stream_filter_append()函数可以把过滤器附加到流上。
$handle = fopen('data.txt', 'rb');
stream_filter_append($handle, 'string.toupper');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);
?>
这里stream_filter_append把string.toupper这个filter加到了流了输出的全是大写。
也可以使用php://filter来附加过滤器
$handle = fopen('php://filter/read=string.toupper/resource=data.txt', 'rb');
while (feof($handle) !== true) {
echo fgets($handle);
}
fclose($handle);
?>
这个流标识符如下:
filter/read=
PHP某些文件系统函数在调用后无法附加过滤器,例如file(),所以这些函数使用时只能用php://filter封装协议附加流过滤器。
Example:
$dateStart = new \DateTime(); // current date
$dateInterval = \DateInterval::createFromDateString('-1 day'); // get date interval
$datePeriod = new \DatePeriod($dateStart, $dateInterval, 30); // get the period between dates
foreach ($datePeriod as $date) {
$file = "sftp://USER:[email protected]" . $date->format('Y-m-d') . 'log.bz2';
if (file_exists($file)) {
$handle = fopen($file, 'rb'); // read the file into stream
stream_filter_append($handle, 'bzip2.decompress'); // add a filter to stream to decompress the zip file
while (feof($handle) !== true) {
$line = fgets($handle);
if (strpos($line, 'www.example.com') !== false) { // if find the address in the line
fwrite(STDOUT, $line); // output the line
}
}
fclose($handle);
}
}
?>
自定义流过滤器:
class DirtyWordsFilter extends php_user_filter
{
/**
* @param resource $in 流来的桶队列
* @param resource $out 流走的桶队列
* @param int $consumed 处理的字节数
* @param bool $closing 是流中最后一个桶队列么?
*/
public function filter($in, $out, &$consumed, $closing)
{
$words = array('grime', 'dirt', 'grease');
$wordData = array();
foreach ($words as $word) {
$replacement = array_fill(0, mb_strlen($word), '*');
$wordData[$word] = implode('', $replacement);
}
$bad = array_keys($wordData);
$good = array_values($wordData);
while ($bucket = stream_bucket_make_writeable($in)) {
// 检查桶数据
$bucket->data = str_replace($bad, $good, $bucket->data);
// 增加已处理的数据量
$consume += $backet->datalen;
// 把桶放入流向下游队列
$stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
?>
filter()方法的作用是接收,处理再转运桶中的数据流。在filter()方法中,我们迭代桶队列$in中的桶,把脏字替换成审查后的值。这个方法的返回值是PSFS_PASS_ON常量,表示操作成功。
$in 上游流来的一个队列,有一个或多个桶,桶中是从出发地流来的数据
$out 由一个桶或多个桶组成的队列,流向下游的流目的地
&$consumed 自定义的过滤器处理的流数据总字节数
$closing filter()方法接受的是最后一个桶队列么?
然后还需要注册自定义的DirtyWordsFilter流过滤器
stream_filter_register('dirty_words_filter', 'DirtyWordsFilter');
第一个参数是用于识别这个自定义过滤器的过滤名,第二个参数是这个自定义过滤器的类名。现在可以使用这个自定义的流过滤器。