为什么不应该使用mysql_*函数的技术原因是什么?(例如mysql_query()、mysql_connect()或mysql_real_ascape_string())?
为什么我要使用其他东西,即使它们在我的网站上工作?
如果它们在我的网站上不起作用,为什么我会出现这样的错误
警告:mysql_connect():没有这样的文件或目录
为什么不应该使用mysql_*函数的技术原因是什么?(例如mysql_query()、mysql_connect()或mysql_real_ascape_string())?
为什么我要使用其他东西,即使它们在我的网站上工作?
如果它们在我的网站上不起作用,为什么我会出现这样的错误
警告:mysql_connect():没有这样的文件或目录
当前回答
PHP提供了三种不同的API来连接MySQL。这些是mysql(自PHP7起删除)、mysqli和PDO扩展。
mysql_*函数曾经非常流行,但现在已经不鼓励使用了。文档团队正在讨论数据库安全情况,教育用户远离常用的ext/mysql扩展也是其中的一部分(请检查php.internals:弃用ext/mysql)。
后来的PHP开发团队决定在用户连接到MySQL时生成E_DEPRECATED错误,无论是通过MySQL_connect()、MySQL_pconnect()还是ext/MySQL内置的隐式连接功能。
ext/mysql从PHP 5.5开始被正式弃用,从PHP 7开始被删除。
看到红盒子了吗?
当您进入任何mysql_*函数手册页面时,都会看到一个红色框,说明它不应该再使用。
Why
离开ext/mysql不仅仅是为了安全性,还为了能够访问mysql数据库的所有特性。
ext/mysql是为MySQL3.23构建的,从那时起只添加了很少的内容,但大部分都保持了与这个旧版本的兼容性,这使得代码有点难以维护。ext/mysql不支持的缺失功能包括:(来自PHP手册)。
存储过程(无法处理多个结果集)编制的报表加密(SSL)压缩完整字符集支持
不使用mysql_*函数的原因:
未积极开发自PHP 7起删除缺少OO接口不支持非阻塞异步查询不支持准备好的语句或参数化查询不支持存储过程不支持多个语句不支持事务不支持MySQL 5.1中的所有功能
以上引用了昆汀的回答
缺少对准备好的语句的支持尤为重要,因为与使用单独的函数调用手动转义外部数据相比,它们提供了一种更清晰、更不易出错的转义和引用外部数据的方法。
请参阅SQL扩展的比较。
取消弃用警告
当代码转换为MySQLi/PDO时,通过在php.ini中设置error_reporting以排除E_DEPRECATED,可以抑制E_DEPRECATE错误:
error_reporting = E_ALL ^ E_DEPRECATED
注意,这也会隐藏其他弃用警告,但是,这些警告可能针对MySQL以外的其他内容。(摘自PHP手册)
文章PDO与MySQLi:你应该使用哪一个?Dejan Marjanovic将帮助您选择。
更好的方法是PDO,我现在正在编写一个简单的PDO教程。
简单简短的PDO教程
问:我脑海中的第一个问题是:什么是“PDO”?
答:“PDO——PHP数据对象——是一个数据库访问层,提供了对多个数据库的统一访问方法。”
连接到MySQL
使用mysql_*函数,或者我们可以用旧方法(在PHP 5.5及以上版本中已弃用)
$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);
使用PDO:您需要做的就是创建一个新的PDO对象。构造函数接受用于指定数据库源的参数。PDO的构造函数主要接受四个参数,即DSN(数据源名称)和可选的用户名和密码。
在这里,我认为您对除DSN之外的所有内容都很熟悉;这在PDO中是新的。DSN基本上是一系列选项,它们告诉PDO要使用哪个驱动程序以及连接详细信息。有关进一步参考,请检查PDO MySQL DSN。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');
注意:您也可以使用charset=UTF-8,但有时会导致错误,所以最好使用utf8。
如果有任何连接错误,它将抛出一个PDOException对象,该对象可以被捕获以进一步处理Exception。
良好阅读:连接和连接管理¶
还可以将多个驱动程序选项作为数组传递给第四个参数。我建议传递将PDO置于异常模式的参数。因为一些PDO驱动程序不支持本机准备语句,所以PDO执行准备的模拟。它还允许您手动启用此仿真。要使用本机服务器端准备的语句,应显式将其设置为false。
另一种方法是关闭MySQL驱动程序中默认启用的准备模拟,但应关闭准备模拟以安全地使用PDO。
稍后我将解释为什么要关闭准备模拟。要找到原因,请查看此帖子。
只有当您使用的是我不建议使用的旧版本MySQL时,它才可用。
下面是一个如何做到的示例:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password',
array(PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
我们可以在PDO构建之后设置属性吗?
是的,我们也可以使用setAttribute方法在PDO构造后设置一些属性:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
错误处理
PDO中的错误处理比mysql_*容易得多。
使用mysql_*时的一个常见做法是:
//Connected to MySQL
$result = mysql_query("SELECT * FROM table", $link) or die(mysql_error($link));
OR die()不是处理错误的好方法,因为我们无法处理die中的事情。它只会突然结束脚本,然后将错误反映到屏幕上,而您通常不想向最终用户显示该错误,并让血腥的黑客发现您的模式。或者,mysql_*函数的返回值通常可以与mysql_error()结合使用来处理错误。
PDO提供了更好的解决方案:异常。我们用PDO做的任何事情都应该封装在try-catch块中。通过设置错误模式属性,我们可以强制PDO进入三种错误模式之一。以下是三种错误处理模式。
PDO::ERRMODE_SILENT。它只是设置错误代码,其行为与mysql_*几乎相同,您必须检查每个结果,然后查看$db->errorInfo();以获取错误详细信息。PDO::ERRMODE_WARNING引发E_WARNING。(运行时警告(非致命错误)。脚本的执行不会停止。)PDO::ERRMODE_EXCEPTION:引发异常。它表示PDO引发的错误。您不应该从自己的代码中抛出PDOException。有关PHP中异常的更多信息,请参阅异常。它的行为非常像或死(mysql_error());,当它没有被抓住时。但与or die()不同,如果您选择这样做,PDOException可以被捕获并优雅地处理。
读起来不错:
错误和错误处理¶PDOException类¶例外¶
喜欢:
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
您可以将其包装在try-catch中,如下所示:
try {
//Connect as appropriate as above
$db->query('hi'); //Invalid query!
}
catch (PDOException $ex) {
echo "An Error occured!"; //User friendly message/message you want to show to user
some_logging_function($ex->getMessage());
}
你现在不必处理try-catch。您可以在任何合适的时间捕捉它,但我强烈建议您使用try-catch。此外,在调用PDO内容的函数外部捕获它可能更有意义:
function data_fun($db) {
$stmt = $db->query("SELECT * FROM table");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
//Then later
try {
data_fun($db);
}
catch(PDOException $ex) {
//Here you can handle error and show message/perform action you want.
}
此外,您可以处理by或die(),或者我们可以说像mysql_*,但它会非常多样化。通过关闭display_errors并读取错误日志,可以在生产中隐藏危险的错误消息。
现在,在阅读了以上所有内容之后,您可能会想:当我只想开始学习简单的SELECT、INSERT、UPDATE或DELETE语句时,这到底是什么?别担心,我们来了:
选择数据
因此,您在mysql_*中所做的是:
<?php
$result = mysql_query('SELECT * from table') or die(mysql_error());
$num_rows = mysql_num_rows($result);
while($row = mysql_fetch_assoc($result)) {
echo $row['field1'];
}
现在在PDO中,您可以这样做:
<?php
$stmt = $db->query('SELECT * FROM table');
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo $row['field1'];
}
Or
<?php
$stmt = $db->query('SELECT * FROM table');
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
//Use $results
注意:如果使用下面的方法(query()),此方法将返回PDOStatement对象。因此,如果您想获取结果,请像上面那样使用它。
<?php
foreach($db->query('SELECT * FROM table') as $row) {
echo $row['field1'];
}
在PDOData中,它是通过->fetch()获得的,这是一个语句句柄的方法。在调用fetch之前,最好的方法是告诉PDO您希望如何获取数据。在下面的部分中,我将对此进行解释。
提取模式
注意在上面的FETCH()和fetchAll()代码中使用了PDO::FETCH_ASSOC。这告诉PDO以关联数组的形式返回行,字段名作为键。还有许多其他提取模式,我将逐一解释。
首先,我解释如何选择提取模式:
$stmt->fetch(PDO::FETCH_ASSOC)
在上文中,我一直在使用fetch()。您还可以使用:
PDOStatement::fetchAll()-返回包含所有结果集行的数组PDOStatement::fetchColumn()-从结果集的下一行返回一列PDOStatement::fetchObject()-获取下一行并将其作为对象返回。PDOStatement::setFetchMode()-设置此语句的默认获取模式
现在我进入提取模式:
PDO::FETCH_ASSOC:返回结果集中返回的按列名索引的数组PDO::FETCH_BOTH(默认值):返回一个按结果集中返回的列名和0索引列编号索引的数组
还有更多的选择!请阅读PDOStatement Fetch文档中的所有内容。。
获取行计数:
您可以获取PDOStatement并执行rowCount(),而不是使用mysql_num_rows来获取返回的行数,例如:
<?php
$stmt = $db->query('SELECT * FROM table');
$row_count = $stmt->rowCount();
echo $row_count.' rows selected';
获取上次插入的ID
<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();
插入和更新或删除语句
我们在mysql_*函数中所做的是:
<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);
在pdo中,同样的事情可以通过以下方式完成:
<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;
在上述查询中,PDO::exec执行SQL语句并返回受影响的行数。
稍后将介绍插入和删除。
上述方法仅在查询中不使用变量时有用。但当您需要在查询中使用变量时,千万不要像上面那样尝试,因为准备好的语句或参数化语句就是这样。
编制的报表
问:什么是准备好的声明,为什么我需要它们?答:准备好的语句是预编译的SQL语句,只需将数据发送到服务器即可多次执行。
使用准备好的语句的典型工作流程如下(引自维基百科三点):
准备:语句模板由应用程序创建并发送到数据库管理系统(DBMS)。某些值未指定,称为参数、占位符或绑定变量(以下标记为?):插入产品(名称、价格)值(?,?)DBMS对语句模板进行分析、编译和执行查询优化,并在不执行结果的情况下存储结果。执行:稍后,应用程序为参数提供(或绑定)值,DBMS执行语句(可能返回结果)。应用程序可以使用不同的值执行任意次数的语句。在本例中,它可能为第一个参数提供“面包”,为第二个参数提供1.00。
您可以通过在SQL中包含占位符来使用准备好的语句。基本上有三个没有占位符(不要尝试使用上面的变量its),一个没有命名占位符,一个有命名占位符。
问:现在,什么是命名占位符,我如何使用它们?A.命名占位符。使用冒号前面的描述性名称,而不是问号。我们不关心名称占位符中的位置/价值顺序:
$stmt->bindParam(':bla', $bla);
bindParam(参数、变量、data_type、长度、驱动程序选项)
也可以使用执行数组进行绑定:
<?php
$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name");
$stmt->execute(array(':name' => $name, ':id' => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
OOP朋友的另一个很好的特性是,命名占位符可以直接将对象插入数据库,前提是财产与命名字段匹配。例如:
class person {
public $name;
public $add;
function __construct($a,$b) {
$this->name = $a;
$this->add = $b;
}
}
$demo = new person('john','29 bla district');
$stmt = $db->prepare("INSERT INTO table (name, add) value (:name, :add)");
$stmt->execute((array)$demo);
问:现在,什么是未命名占位符,我如何使用它们?A.让我们举个例子:
<?php
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $add, PDO::PARAM_STR);
$stmt->execute();
and
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));
在上面,你可以看到这些?而不是像名字占位符那样的名字。现在在第一个示例中,我们将变量分配给各种占位符($stmt->bindValue(1,$name,PDO::PARAM_STR);)。然后,我们将值分配给这些占位符并执行该语句。在第二个示例中,第一个数组元素转到第一个?第二个到第二个?。
注意:在未命名占位符中,我们必须注意传递给PDOState::execute()方法的数组中元素的正确顺序。
SELECT、INSERT、UPDATE、DELETE准备好的查询
选择:$stmt=$db->prepare(“SELECT*FROM table WHERE id=:id AND name=:name”);$stmt->execute(数组(“:name”=>$name,“:id”=>$id));$rows=$stmt->fetchAll(PDO::FETCH_ASSOC);插入:$stmt=$db->prepare(“INSERT INTO table(field1,field2)VALUES(:field1,:field2)”);$stmt->execute(数组(':field1'=>$field1,':field2'=>$field2));$affected_rows=$stmt->rowCount();删除:$stmt=$db->prepare(“DELETE FROM table WHERE id=:id”);$stmt->bindValue(“:id”,$id,PDO::PARAM_STR);$stmt->execute();$affected_rows=$stmt->rowCount();更新:$stmt=$db->prepare(“UPDATE table SET name=?WHERE id=?”);$stmt->execute(数组($name,$id));$affected_rows=$stmt->rowCount();
注:
然而,PDO和/或MySQLi并不完全安全。检查答案PDO准备的语句是否足以防止SQL注入?由ircmaxell设计。此外,我引用了他的回答中的部分内容:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES GBK');
$stmt = $pdo->prepare("SELECT * FROM test WHERE name = ? LIMIT 1");
$stmt->execute(array(chr(0xbf) . chr(0x27) . " OR 1=1 /*"));
其他回答
编写这个答案是为了说明绕过编写糟糕的PHP用户验证代码是多么微不足道,这些攻击是如何工作的(以及使用什么),以及如何用一个安全的准备好的语句替换旧的MySQL函数,以及基本上,为什么StackOverflow用户(可能有很多代表)会对新用户狂吠,询问如何改进他们的代码。
首先,请随意创建这个测试mysql数据库(我已经调用了我的prep):
mysql> create table users(
-> id int(2) primary key auto_increment,
-> userid tinytext,
-> pass tinytext);
Query OK, 0 rows affected (0.05 sec)
mysql> insert into users values(null, 'Fluffeh', 'mypass');
Query OK, 1 row affected (0.04 sec)
mysql> create user 'prepared'@'localhost' identified by 'example';
Query OK, 0 rows affected (0.01 sec)
mysql> grant all privileges on prep.* to 'prepared'@'localhost' with grant option;
Query OK, 0 rows affected (0.00 sec)
完成后,我们可以转到PHP代码。
让我们假设以下脚本是网站管理员的验证过程(如果您复制并将其用于测试,则简化但有效):
<?php
if(!empty($_POST['user']))
{
$user=$_POST['user'];
}
else
{
$user='bob';
}
if(!empty($_POST['pass']))
{
$pass=$_POST['pass'];
}
else
{
$pass='bob';
}
$database='prep';
$link=mysql_connect('localhost', 'prepared', 'example');
mysql_select_db($database) or die( "Unable to select database");
$sql="select id, userid, pass from users where userid='$user' and pass='$pass'";
//echo $sql."<br><br>";
$result=mysql_query($sql);
$isAdmin=false;
while ($row = mysql_fetch_assoc($result)) {
echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
$isAdmin=true;
// We have correctly matched the Username and Password
// Lets give this person full access
}
if($isAdmin)
{
echo "The check passed. We have a verified admin!<br>";
}
else
{
echo "You could not be verified. Please try again...<br>";
}
mysql_close($link);
?>
<form name="exploited" method='post'>
User: <input type='text' name='user'><br>
Pass: <input type='text' name='pass'><br>
<input type='submit'>
</form>
乍一看似乎足够合理。
用户必须输入登录名和密码,对吗?
Brilliant,现在输入以下内容:
user: bob
pass: somePass
并提交。
输出如下:
You could not be verified. Please try again...
超级的按照预期工作,现在让我们尝试实际的用户名和密码:
user: Fluffeh
pass: mypass
太神了大家好,代码正确地验证了管理员。太完美了!
嗯,不是真的。假设用户是一个聪明的小人物。让我们说这个人是我。
输入以下内容:
user: bob
pass: n' or 1=1 or 'm=m
输出为:
The check passed. We have a verified admin!
恭喜你,你刚刚允许我输入一个假用户名和一个假密码,进入你的超级保护管理员专用部分。说真的,如果你不相信我的话,用我提供的代码创建数据库,然后运行这个PHP代码——乍一看,它确实很好地验证了用户名和密码。
所以,作为回答,这就是为什么你被黄了。
所以,让我们看看出了什么问题,以及为什么我刚刚进入了你的超级管理员专用蝙蝠洞。我猜了一下,假设你对输入不小心,直接将它们传递到数据库。我构建输入的方式会改变您实际运行的查询。那么,它应该是什么,最终是什么?
select id, userid, pass from users where userid='$user' and pass='$pass'
这是一个查询,但当我们用实际使用的输入替换变量时,我们会得到以下结果:
select id, userid, pass from users where userid='bob' and pass='n' or 1=1 or 'm=m'
看看我是如何构造我的“密码”的,这样它将首先关闭密码周围的单引号,然后引入一个全新的比较?然后为了安全起见,我添加了另一个“字符串”,这样单引号就可以像我们最初的代码中所期望的那样关闭。
然而,这不是人们现在对你大喊大叫,而是向你展示如何使代码更安全。
好吧,那么出了什么问题,我们该怎么解决?
这是典型的SQL注入攻击。这是最简单的方法之一。在攻击向量的规模上,这是一个蹒跚学步的孩子攻击坦克并获胜。
那么,我们如何保护您神圣的管理部分并使其变得美观和安全?首先要做的是停止使用那些非常陈旧和过时的mysql_*函数。我知道,你遵循了你在网上找到的一个教程,它很有用,但它很旧,很过时,几分钟内,我就突破了它,甚至连汗都没流。
现在,您可以使用mysqli_或PDO了。我个人是PDO的忠实粉丝,因此我将在本答案的其余部分使用PDO。有赞成者和反对者,但我个人认为赞成者远远超过反对者。它可以跨多个数据库引擎进行移植-无论您使用的是MySQL还是Oracle,或者几乎是任何该死的东西-只需更改连接字符串,它就拥有了我们想要使用的所有花哨功能,而且非常干净。我喜欢干净。
现在,让我们再次看看这段代码,这次是使用PDO对象编写的:
<?php
if(!empty($_POST['user']))
{
$user=$_POST['user'];
}
else
{
$user='bob';
}
if(!empty($_POST['pass']))
{
$pass=$_POST['pass'];
}
else
{
$pass='bob';
}
$isAdmin=false;
$database='prep';
$pdo=new PDO ('mysql:host=localhost;dbname=prep', 'prepared', 'example');
$sql="select id, userid, pass from users where userid=:user and pass=:password";
$myPDO = $pdo->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
if($myPDO->execute(array(':user' => $user, ':password' => $pass)))
{
while($row=$myPDO->fetch(PDO::FETCH_ASSOC))
{
echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
$isAdmin=true;
// We have correctly matched the Username and Password
// Lets give this person full access
}
}
if($isAdmin)
{
echo "The check passed. We have a verified admin!<br>";
}
else
{
echo "You could not be verified. Please try again...<br>";
}
?>
<form name="exploited" method='post'>
User: <input type='text' name='user'><br>
Pass: <input type='text' name='pass'><br>
<input type='submit'>
</form>
主要区别在于不再有mysql_*函数。这一切都是通过PDO对象完成的,其次,它使用了一个准备好的语句。现在,你问一个准备好的陈述是什么?这是一种在运行查询之前告诉数据库我们要运行的查询的方式。在这种情况下,我们告诉数据库:“嗨,我要运行一个select语句,想要表用户的id、userid和pass,其中userid是一个变量,pass也是一个变量”。
然后,在execute语句中,我们向数据库传递一个数组,其中包含它现在需要的所有变量。
结果太棒了。让我们再次尝试以前的用户名和密码组合:
user: bob
pass: somePass
未验证用户。令人惊叹的
怎么样:
user: Fluffeh
pass: mypass
哦,我只是有点兴奋,它奏效了:支票通过了。我们有一个经过验证的管理员!
现在,让我们来试试聪明的家伙会输入的数据,以试图通过我们的小验证系统:
user: bob
pass: n' or 1=1 or 'm=m
这次,我们得到了以下结果:
You could not be verified. Please try again...
这就是为什么你在发布问题时会被吼——这是因为人们可以看到你的代码可以被绕过,甚至不用尝试。请使用这个问题和答案来改进代码,使其更安全,并使用当前的功能。
最后,这并不是说这是完美的代码。你可以做更多的事情来改进它,例如,使用散列密码,确保当你在数据库中存储敏感信息时,你不会以纯文本形式存储,并具有多个级别的验证-但实际上,如果你只是将旧的易注入代码改为这样,在编写好代码的过程中,你会很好的——事实上,你已经走到了这一步,而且还在阅读,这让我有一种希望,你不仅会在编写网站和应用程序时实现这种类型的代码,而且你可能会出去研究我刚才提到的其他东西——还有更多。写出你能写的最好的代码,而不是最基本的代码。
PHP提供了三种不同的API来连接MySQL。这些是mysql(自PHP7起删除)、mysqli和PDO扩展。
mysql_*函数曾经非常流行,但现在已经不鼓励使用了。文档团队正在讨论数据库安全情况,教育用户远离常用的ext/mysql扩展也是其中的一部分(请检查php.internals:弃用ext/mysql)。
后来的PHP开发团队决定在用户连接到MySQL时生成E_DEPRECATED错误,无论是通过MySQL_connect()、MySQL_pconnect()还是ext/MySQL内置的隐式连接功能。
ext/mysql从PHP 5.5开始被正式弃用,从PHP 7开始被删除。
看到红盒子了吗?
当您进入任何mysql_*函数手册页面时,都会看到一个红色框,说明它不应该再使用。
Why
离开ext/mysql不仅仅是为了安全性,还为了能够访问mysql数据库的所有特性。
ext/mysql是为MySQL3.23构建的,从那时起只添加了很少的内容,但大部分都保持了与这个旧版本的兼容性,这使得代码有点难以维护。ext/mysql不支持的缺失功能包括:(来自PHP手册)。
存储过程(无法处理多个结果集)编制的报表加密(SSL)压缩完整字符集支持
不使用mysql_*函数的原因:
未积极开发自PHP 7起删除缺少OO接口不支持非阻塞异步查询不支持准备好的语句或参数化查询不支持存储过程不支持多个语句不支持事务不支持MySQL 5.1中的所有功能
以上引用了昆汀的回答
缺少对准备好的语句的支持尤为重要,因为与使用单独的函数调用手动转义外部数据相比,它们提供了一种更清晰、更不易出错的转义和引用外部数据的方法。
请参阅SQL扩展的比较。
取消弃用警告
当代码转换为MySQLi/PDO时,通过在php.ini中设置error_reporting以排除E_DEPRECATED,可以抑制E_DEPRECATE错误:
error_reporting = E_ALL ^ E_DEPRECATED
注意,这也会隐藏其他弃用警告,但是,这些警告可能针对MySQL以外的其他内容。(摘自PHP手册)
文章PDO与MySQLi:你应该使用哪一个?Dejan Marjanovic将帮助您选择。
更好的方法是PDO,我现在正在编写一个简单的PDO教程。
简单简短的PDO教程
问:我脑海中的第一个问题是:什么是“PDO”?
答:“PDO——PHP数据对象——是一个数据库访问层,提供了对多个数据库的统一访问方法。”
连接到MySQL
使用mysql_*函数,或者我们可以用旧方法(在PHP 5.5及以上版本中已弃用)
$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);
使用PDO:您需要做的就是创建一个新的PDO对象。构造函数接受用于指定数据库源的参数。PDO的构造函数主要接受四个参数,即DSN(数据源名称)和可选的用户名和密码。
在这里,我认为您对除DSN之外的所有内容都很熟悉;这在PDO中是新的。DSN基本上是一系列选项,它们告诉PDO要使用哪个驱动程序以及连接详细信息。有关进一步参考,请检查PDO MySQL DSN。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');
注意:您也可以使用charset=UTF-8,但有时会导致错误,所以最好使用utf8。
如果有任何连接错误,它将抛出一个PDOException对象,该对象可以被捕获以进一步处理Exception。
良好阅读:连接和连接管理¶
还可以将多个驱动程序选项作为数组传递给第四个参数。我建议传递将PDO置于异常模式的参数。因为一些PDO驱动程序不支持本机准备语句,所以PDO执行准备的模拟。它还允许您手动启用此仿真。要使用本机服务器端准备的语句,应显式将其设置为false。
另一种方法是关闭MySQL驱动程序中默认启用的准备模拟,但应关闭准备模拟以安全地使用PDO。
稍后我将解释为什么要关闭准备模拟。要找到原因,请查看此帖子。
只有当您使用的是我不建议使用的旧版本MySQL时,它才可用。
下面是一个如何做到的示例:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password',
array(PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
我们可以在PDO构建之后设置属性吗?
是的,我们也可以使用setAttribute方法在PDO构造后设置一些属性:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8',
'username',
'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
错误处理
PDO中的错误处理比mysql_*容易得多。
使用mysql_*时的一个常见做法是:
//Connected to MySQL
$result = mysql_query("SELECT * FROM table", $link) or die(mysql_error($link));
OR die()不是处理错误的好方法,因为我们无法处理die中的事情。它只会突然结束脚本,然后将错误反映到屏幕上,而您通常不想向最终用户显示该错误,并让血腥的黑客发现您的模式。或者,mysql_*函数的返回值通常可以与mysql_error()结合使用来处理错误。
PDO提供了更好的解决方案:异常。我们用PDO做的任何事情都应该封装在try-catch块中。通过设置错误模式属性,我们可以强制PDO进入三种错误模式之一。以下是三种错误处理模式。
PDO::ERRMODE_SILENT。它只是设置错误代码,其行为与mysql_*几乎相同,您必须检查每个结果,然后查看$db->errorInfo();以获取错误详细信息。PDO::ERRMODE_WARNING引发E_WARNING。(运行时警告(非致命错误)。脚本的执行不会停止。)PDO::ERRMODE_EXCEPTION:引发异常。它表示PDO引发的错误。您不应该从自己的代码中抛出PDOException。有关PHP中异常的更多信息,请参阅异常。它的行为非常像或死(mysql_error());,当它没有被抓住时。但与or die()不同,如果您选择这样做,PDOException可以被捕获并优雅地处理。
读起来不错:
错误和错误处理¶PDOException类¶例外¶
喜欢:
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
您可以将其包装在try-catch中,如下所示:
try {
//Connect as appropriate as above
$db->query('hi'); //Invalid query!
}
catch (PDOException $ex) {
echo "An Error occured!"; //User friendly message/message you want to show to user
some_logging_function($ex->getMessage());
}
你现在不必处理try-catch。您可以在任何合适的时间捕捉它,但我强烈建议您使用try-catch。此外,在调用PDO内容的函数外部捕获它可能更有意义:
function data_fun($db) {
$stmt = $db->query("SELECT * FROM table");
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
//Then later
try {
data_fun($db);
}
catch(PDOException $ex) {
//Here you can handle error and show message/perform action you want.
}
此外,您可以处理by或die(),或者我们可以说像mysql_*,但它会非常多样化。通过关闭display_errors并读取错误日志,可以在生产中隐藏危险的错误消息。
现在,在阅读了以上所有内容之后,您可能会想:当我只想开始学习简单的SELECT、INSERT、UPDATE或DELETE语句时,这到底是什么?别担心,我们来了:
选择数据
因此,您在mysql_*中所做的是:
<?php
$result = mysql_query('SELECT * from table') or die(mysql_error());
$num_rows = mysql_num_rows($result);
while($row = mysql_fetch_assoc($result)) {
echo $row['field1'];
}
现在在PDO中,您可以这样做:
<?php
$stmt = $db->query('SELECT * FROM table');
while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo $row['field1'];
}
Or
<?php
$stmt = $db->query('SELECT * FROM table');
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
//Use $results
注意:如果使用下面的方法(query()),此方法将返回PDOStatement对象。因此,如果您想获取结果,请像上面那样使用它。
<?php
foreach($db->query('SELECT * FROM table') as $row) {
echo $row['field1'];
}
在PDOData中,它是通过->fetch()获得的,这是一个语句句柄的方法。在调用fetch之前,最好的方法是告诉PDO您希望如何获取数据。在下面的部分中,我将对此进行解释。
提取模式
注意在上面的FETCH()和fetchAll()代码中使用了PDO::FETCH_ASSOC。这告诉PDO以关联数组的形式返回行,字段名作为键。还有许多其他提取模式,我将逐一解释。
首先,我解释如何选择提取模式:
$stmt->fetch(PDO::FETCH_ASSOC)
在上文中,我一直在使用fetch()。您还可以使用:
PDOStatement::fetchAll()-返回包含所有结果集行的数组PDOStatement::fetchColumn()-从结果集的下一行返回一列PDOStatement::fetchObject()-获取下一行并将其作为对象返回。PDOStatement::setFetchMode()-设置此语句的默认获取模式
现在我进入提取模式:
PDO::FETCH_ASSOC:返回结果集中返回的按列名索引的数组PDO::FETCH_BOTH(默认值):返回一个按结果集中返回的列名和0索引列编号索引的数组
还有更多的选择!请阅读PDOStatement Fetch文档中的所有内容。。
获取行计数:
您可以获取PDOStatement并执行rowCount(),而不是使用mysql_num_rows来获取返回的行数,例如:
<?php
$stmt = $db->query('SELECT * FROM table');
$row_count = $stmt->rowCount();
echo $row_count.' rows selected';
获取上次插入的ID
<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();
插入和更新或删除语句
我们在mysql_*函数中所做的是:
<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);
在pdo中,同样的事情可以通过以下方式完成:
<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;
在上述查询中,PDO::exec执行SQL语句并返回受影响的行数。
稍后将介绍插入和删除。
上述方法仅在查询中不使用变量时有用。但当您需要在查询中使用变量时,千万不要像上面那样尝试,因为准备好的语句或参数化语句就是这样。
编制的报表
问:什么是准备好的声明,为什么我需要它们?答:准备好的语句是预编译的SQL语句,只需将数据发送到服务器即可多次执行。
使用准备好的语句的典型工作流程如下(引自维基百科三点):
准备:语句模板由应用程序创建并发送到数据库管理系统(DBMS)。某些值未指定,称为参数、占位符或绑定变量(以下标记为?):插入产品(名称、价格)值(?,?)DBMS对语句模板进行分析、编译和执行查询优化,并在不执行结果的情况下存储结果。执行:稍后,应用程序为参数提供(或绑定)值,DBMS执行语句(可能返回结果)。应用程序可以使用不同的值执行任意次数的语句。在本例中,它可能为第一个参数提供“面包”,为第二个参数提供1.00。
您可以通过在SQL中包含占位符来使用准备好的语句。基本上有三个没有占位符(不要尝试使用上面的变量its),一个没有命名占位符,一个有命名占位符。
问:现在,什么是命名占位符,我如何使用它们?A.命名占位符。使用冒号前面的描述性名称,而不是问号。我们不关心名称占位符中的位置/价值顺序:
$stmt->bindParam(':bla', $bla);
bindParam(参数、变量、data_type、长度、驱动程序选项)
也可以使用执行数组进行绑定:
<?php
$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name");
$stmt->execute(array(':name' => $name, ':id' => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
OOP朋友的另一个很好的特性是,命名占位符可以直接将对象插入数据库,前提是财产与命名字段匹配。例如:
class person {
public $name;
public $add;
function __construct($a,$b) {
$this->name = $a;
$this->add = $b;
}
}
$demo = new person('john','29 bla district');
$stmt = $db->prepare("INSERT INTO table (name, add) value (:name, :add)");
$stmt->execute((array)$demo);
问:现在,什么是未命名占位符,我如何使用它们?A.让我们举个例子:
<?php
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $add, PDO::PARAM_STR);
$stmt->execute();
and
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));
在上面,你可以看到这些?而不是像名字占位符那样的名字。现在在第一个示例中,我们将变量分配给各种占位符($stmt->bindValue(1,$name,PDO::PARAM_STR);)。然后,我们将值分配给这些占位符并执行该语句。在第二个示例中,第一个数组元素转到第一个?第二个到第二个?。
注意:在未命名占位符中,我们必须注意传递给PDOState::execute()方法的数组中元素的正确顺序。
SELECT、INSERT、UPDATE、DELETE准备好的查询
选择:$stmt=$db->prepare(“SELECT*FROM table WHERE id=:id AND name=:name”);$stmt->execute(数组(“:name”=>$name,“:id”=>$id));$rows=$stmt->fetchAll(PDO::FETCH_ASSOC);插入:$stmt=$db->prepare(“INSERT INTO table(field1,field2)VALUES(:field1,:field2)”);$stmt->execute(数组(':field1'=>$field1,':field2'=>$field2));$affected_rows=$stmt->rowCount();删除:$stmt=$db->prepare(“DELETE FROM table WHERE id=:id”);$stmt->bindValue(“:id”,$id,PDO::PARAM_STR);$stmt->execute();$affected_rows=$stmt->rowCount();更新:$stmt=$db->prepare(“UPDATE table SET name=?WHERE id=?”);$stmt->execute(数组($name,$id));$affected_rows=$stmt->rowCount();
注:
然而,PDO和/或MySQLi并不完全安全。检查答案PDO准备的语句是否足以防止SQL注入?由ircmaxell设计。此外,我引用了他的回答中的部分内容:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES GBK');
$stmt = $pdo->prepare("SELECT * FROM test WHERE name = ? LIMIT 1");
$stmt->execute(array(chr(0xbf) . chr(0x27) . " OR 1=1 /*"));
MySQL扩展:
未积极开发自PHP 5.5(2013年6月发布)起正式弃用。自PHP 7.0(2015年12月发布)起已完全删除这意味着截至2018年12月31日,它不存在于任何支持的PHP版本中。如果您使用的是支持它的PHP版本,那么您使用的版本不会解决安全问题。缺少OO接口不支持:非阻塞异步查询准备好的语句或参数化查询存储过程多个语句交易“新”密码身份验证方法(默认情况下,在MySQL 5.6中打开;5.7中需要)MySQL 5.1或更高版本中的任何新功能
由于它已被弃用,因此使用它会使您的代码不那么经得起未来考验。
缺少对准备好的语句的支持尤为重要,因为与使用单独的函数调用手动转义外部数据相比,它们提供了一种更清晰、更不易出错的转义和引用外部数据的方法。
请参阅SQL扩展的比较。
原因有很多,但可能最重要的一点是,这些函数鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止SQL注入攻击。
当使用mysql_*函数时,您必须记住通过mysql_real_aescape_string()运行用户提供的参数。如果你只在一个地方忘记了,或者你碰巧只漏掉了部分输入,你的数据库可能会受到攻击。
在PDO或mysqli中使用已准备好的语句将使其更难产生此类编程错误。
可以使用mysqli或PDO定义几乎所有的mysql_*函数。只需在旧的PHP应用程序上包含它们,它就可以在PHP7上运行。我的解决方案在这里。
<?php
define('MYSQL_LINK', 'dbl');
$GLOBALS[MYSQL_LINK] = null;
function mysql_link($link=null) {
return ($link === null) ? $GLOBALS[MYSQL_LINK] : $link;
}
function mysql_connect($host, $user, $pass) {
$GLOBALS[MYSQL_LINK] = mysqli_connect($host, $user, $pass);
return $GLOBALS[MYSQL_LINK];
}
function mysql_pconnect($host, $user, $pass) {
return mysql_connect($host, $user, $pass);
}
function mysql_select_db($db, $link=null) {
$link = mysql_link($link);
return mysqli_select_db($link, $db);
}
function mysql_close($link=null) {
$link = mysql_link($link);
return mysqli_close($link);
}
function mysql_error($link=null) {
$link = mysql_link($link);
return mysqli_error($link);
}
function mysql_errno($link=null) {
$link = mysql_link($link);
return mysqli_errno($link);
}
function mysql_ping($link=null) {
$link = mysql_link($link);
return mysqli_ping($link);
}
function mysql_stat($link=null) {
$link = mysql_link($link);
return mysqli_stat($link);
}
function mysql_affected_rows($link=null) {
$link = mysql_link($link);
return mysqli_affected_rows($link);
}
function mysql_client_encoding($link=null) {
$link = mysql_link($link);
return mysqli_character_set_name($link);
}
function mysql_thread_id($link=null) {
$link = mysql_link($link);
return mysqli_thread_id($link);
}
function mysql_escape_string($string) {
return mysql_real_escape_string($string);
}
function mysql_real_escape_string($string, $link=null) {
$link = mysql_link($link);
return mysqli_real_escape_string($link, $string);
}
function mysql_query($sql, $link=null) {
$link = mysql_link($link);
return mysqli_query($link, $sql);
}
function mysql_unbuffered_query($sql, $link=null) {
$link = mysql_link($link);
return mysqli_query($link, $sql, MYSQLI_USE_RESULT);
}
function mysql_set_charset($charset, $link=null){
$link = mysql_link($link);
return mysqli_set_charset($link, $charset);
}
function mysql_get_host_info($link=null) {
$link = mysql_link($link);
return mysqli_get_host_info($link);
}
function mysql_get_proto_info($link=null) {
$link = mysql_link($link);
return mysqli_get_proto_info($link);
}
function mysql_get_server_info($link=null) {
$link = mysql_link($link);
return mysqli_get_server_info($link);
}
function mysql_info($link=null) {
$link = mysql_link($link);
return mysqli_info($link);
}
function mysql_get_client_info() {
$link = mysql_link();
return mysqli_get_client_info($link);
}
function mysql_create_db($db, $link=null) {
$link = mysql_link($link);
$db = str_replace('`', '', mysqli_real_escape_string($link, $db));
return mysqli_query($link, "CREATE DATABASE `$db`");
}
function mysql_drop_db($db, $link=null) {
$link = mysql_link($link);
$db = str_replace('`', '', mysqli_real_escape_string($link, $db));
return mysqli_query($link, "DROP DATABASE `$db`");
}
function mysql_list_dbs($link=null) {
$link = mysql_link($link);
return mysqli_query($link, "SHOW DATABASES");
}
function mysql_list_fields($db, $table, $link=null) {
$link = mysql_link($link);
$db = str_replace('`', '', mysqli_real_escape_string($link, $db));
$table = str_replace('`', '', mysqli_real_escape_string($link, $table));
return mysqli_query($link, "SHOW COLUMNS FROM `$db`.`$table`");
}
function mysql_list_tables($db, $link=null) {
$link = mysql_link($link);
$db = str_replace('`', '', mysqli_real_escape_string($link, $db));
return mysqli_query($link, "SHOW TABLES FROM `$db`");
}
function mysql_db_query($db, $sql, $link=null) {
$link = mysql_link($link);
mysqli_select_db($link, $db);
return mysqli_query($link, $sql);
}
function mysql_fetch_row($qlink) {
return mysqli_fetch_row($qlink);
}
function mysql_fetch_assoc($qlink) {
return mysqli_fetch_assoc($qlink);
}
function mysql_fetch_array($qlink, $result=MYSQLI_BOTH) {
return mysqli_fetch_array($qlink, $result);
}
function mysql_fetch_lengths($qlink) {
return mysqli_fetch_lengths($qlink);
}
function mysql_insert_id($qlink) {
return mysqli_insert_id($qlink);
}
function mysql_num_rows($qlink) {
return mysqli_num_rows($qlink);
}
function mysql_num_fields($qlink) {
return mysqli_num_fields($qlink);
}
function mysql_data_seek($qlink, $row) {
return mysqli_data_seek($qlink, $row);
}
function mysql_field_seek($qlink, $offset) {
return mysqli_field_seek($qlink, $offset);
}
function mysql_fetch_object($qlink, $class="stdClass", array $params=null) {
return ($params === null)
? mysqli_fetch_object($qlink, $class)
: mysqli_fetch_object($qlink, $class, $params);
}
function mysql_db_name($qlink, $row, $field='Database') {
mysqli_data_seek($qlink, $row);
$db = mysqli_fetch_assoc($qlink);
return $db[$field];
}
function mysql_fetch_field($qlink, $offset=null) {
if ($offset !== null)
mysqli_field_seek($qlink, $offset);
return mysqli_fetch_field($qlink);
}
function mysql_result($qlink, $offset, $field=0) {
if ($offset !== null)
mysqli_field_seek($qlink, $offset);
$row = mysqli_fetch_array($qlink);
return (!is_array($row) || !isset($row[$field]))
? false
: $row[$field];
}
function mysql_field_len($qlink, $offset) {
$field = mysqli_fetch_field_direct($qlink, $offset);
return is_object($field) ? $field->length : false;
}
function mysql_field_name($qlink, $offset) {
$field = mysqli_fetch_field_direct($qlink, $offset);
if (!is_object($field))
return false;
return empty($field->orgname) ? $field->name : $field->orgname;
}
function mysql_field_table($qlink, $offset) {
$field = mysqli_fetch_field_direct($qlink, $offset);
if (!is_object($field))
return false;
return empty($field->orgtable) ? $field->table : $field->orgtable;
}
function mysql_field_type($qlink, $offset) {
$field = mysqli_fetch_field_direct($qlink, $offset);
return is_object($field) ? $field->type : false;
}
function mysql_free_result($qlink) {
try {
mysqli_free_result($qlink);
} catch (Exception $e) {
return false;
}
return true;
}