Answers:
MySQL扩展:
由于不建议使用,因此使用它会使您的代码不再受将来的考验。
缺少对准备好的语句的支持尤其重要,因为与使用单独的函数调用手动转义它们相比,它们提供了一种更清晰,更少出错的转义和引用外部数据的方法。
请参见SQL扩展的比较。
PHP提供了三种不同的API连接到MySQL。这些是mysql
(从PHP 7开始删除的)mysqli
,和PDO
扩展名。
这些mysql_*
功能曾经非常流行,但是不再鼓励使用它们。文档团队正在讨论数据库安全状况,并且教育用户远离常用的ext / mysql扩展是其中一部分(请检查php.internals:不赞成使用ext / mysql)。
而后来的PHP开发团队已产生决定E_DEPRECATED
当用户连接到MySQL的错误,无论是通过mysql_connect()
,mysql_pconnect()
或内置于隐式连接功能ext/mysql
。
ext/mysql
从PHP 5.5开始正式被弃用,从PHP 7开始被删除。
看到红框了吗?
当您进入任何mysql_*
功能手册页面时,都会看到一个红色框,说明不再使用它。
远离ext/mysql
安全不仅关系到安全性,而且关系到访问MySQL数据库的所有功能。
ext/mysql
它是为MySQL 3.23构建的,此后仅增加了很少的内容,同时主要保持了与该旧版本的兼容性,这使得代码难以维护。缺少的功能不支持ext/mysql
包括:(来自PHP手册)。
不使用mysql_*
功能的原因:
缺少对准备好的语句的支持尤其重要,因为与使用单独的函数调用手动转义相比,它们提供了一种更清晰,更易于出错的转义和引用外部数据的方法。
禁止弃用警告
当代码被转换为MySQLi
/时PDO
,E_DEPRECATED
可以通过error_reporting
在php.ini中设置排除错误来抑制错误E_DEPRECATED:
error_reporting = E_ALL ^ E_DEPRECATED
请注意,这还将隐藏其他弃用警告,但是,这可能是针对MySQL以外的内容的。(摘自PHP手册)
文章PDO与库MySQLi:哪你应该使用?由德扬诺维奇将帮助您选择。
更好的方法是PDO
,而我现在正在编写一个简单的PDO
教程。
答:“ PDO – PHP数据对象 –是数据库访问层,提供了访问多个数据库的统一方法。”
使用mysql_*
function或我们可以用旧方式说(在PHP 5.5及更高版本中已弃用)
$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);
使用PDO
:您需要做的就是创建一个新PDO
对象。构造函数接受用于指定数据库源构造函数的参数,该PDO
构造函数主要采用四个参数,分别是DSN
(数据源名称)和(可选username
)password
。
在这里我认为除了大家都熟悉DSN
; 这是新的PDO
。A 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
执行prepare的仿真。它还允许您手动启用此仿真。要使用本机服务器端准备好的语句,应显式设置它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
从自己的代码中抛出A。有关PHP中的异常的更多信息,请参见异常。or die(mysql_error());
未捕获时,它的行为非常像。但是,与不同or die()
,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.
}
另外,您可以按or die()
或说“喜欢” 来处理mysql_*
,但实际情况会有所不同。您可以通过转动display_errors off
并仅阅读错误日志来隐藏生产中的危险错误消息。
现在,在阅读了上面所有的事情之后,你可能在想:到底是什么,当我刚要开始扶着简单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'];
}
要么
<?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'];
}
在PDO数据中,它是通过->fetch()
语句句柄的方法获得的。调用fetch之前,最好的方法是告诉PDO如何获取数据。在下面的部分中,我将对此进行解释。
使用注意事项PDO::FETCH_ASSOC
在fetch()
和fetchAll()
上面的代码。这告诉PDO
将行作为关联数组返回,以字段名称作为键。我也将一一解释其他许多提取模式。
首先,我说明如何选择提取模式:
$stmt->fetch(PDO::FETCH_ASSOC)
在上面,我一直在使用fetch()
。您还可以使用:
PDOStatement::fetchAll()
-返回包含所有结果集行的数组PDOStatement::fetchColumn()
-从结果集的下一行返回一列PDOStatement::fetchObject()
-获取下一行并将其作为对象返回。PDOStatement::setFetchMode()
-设置此语句的默认获取模式现在,我进入获取模式:
PDO::FETCH_ASSOC
:返回结果集中返回的按列名索引的数组PDO::FETCH_BOTH
(默认值):返回结果集中返回的同时由列名和0索引列号索引的数组还有更多选择!在PDOStatement
Fetch文档中阅读所有相关内容。。
获取行数:
mysql_num_rows
您可以使用a PDOStatement
和do 来代替返回的行数rowCount()
,例如:
<?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语句并返回受影响的行数。
插入和删除将在以后介绍。
仅当您在查询中不使用变量时,以上方法才有用。但是,当您需要在查询中使用变量时,请不要尝试像上面那样进行操作,那里有 预处理语句或参数化语句。
问:什么是准备好的声明,为什么我需要它们?
A.准备语句是可以通过只将数据发送到服务器被执行多次预编译的SQL语句。
使用准备好的语句的典型工作流程如下(引自Wikipedia的三分之三):
准备:语句模板由应用程序创建,并发送到数据库管理系统(DBMS)。某些未指定的值称为参数,占位符或绑定变量(?
如下所示):
INSERT INTO PRODUCT (name, price) VALUES (?, ?)
DBMS对语句模板进行解析,编译和查询优化,并在不执行结果的情况下存储结果。
1.00
第二个参数提供“面包” 。您可以通过在SQL中包含占位符来使用准备好的语句。基本上有三种不带占位符的变量(不要在变量上面使用占位符尝试此操作),一种不带占位符的占位符,而另一种具有命名位点。
问:现在,什么叫占位符,我该如何使用它们?
A.命名的占位符。请在描述性名称前加上冒号,而不要使用问号。我们不在乎名称占位符中的位置/值的顺序:
$stmt->bindParam(':bla', $bla);
bindParam(parameter,variable,data_type,length,driver_options)
您还可以使用execute数组进行绑定:
<?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);
问:那么,什么是未命名的占位符以及如何使用它们?
答:让我们举个例子:
<?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();
和
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));
在上面,您可以看到这些,?
而不是像在名称占位符中那样看到名称。现在在第一个示例中,我们将变量分配给各个占位符($stmt->bindValue(1, $name, PDO::PARAM_STR);
)。然后,我们为这些占位符分配值并执行该语句。在第二个示例中,第一个数组元素转到第一个?
,第二个数组元素转到第二个?
。
注意:在未命名的占位符中,我们必须注意传递给PDOStatement::execute()
方法的数组中元素的正确顺序。
SELECT
,INSERT
,UPDATE
,DELETE
准备查询SELECT
:
$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);
INSERT
:
$stmt = $db->prepare("INSERT INTO table(field1,field2) VALUES(:field1,:field2)");
$stmt->execute(array(':field1' => $field1, ':field2' => $field2));
$affected_rows = $stmt->rowCount();
DELETE
:
$stmt = $db->prepare("DELETE FROM table WHERE id=:id");
$stmt->bindValue(':id', $id, PDO::PARAM_STR);
$stmt->execute();
$affected_rows = $stmt->rowCount();
UPDATE
:
$stmt = $db->prepare("UPDATE table SET name=? WHERE id=?");
$stmt->execute(array($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 /*"));
IN (...) construct
。
function throwEx() { throw new Exception("You did selected not existng db"); } mysql_select_db("nonexistdb") or throwEx();
它可以引发异常。
Doesn't support non-blocking, asynchronous queries
不使用mysql_的原因-您也应该列出不使用PDO的原因,因为PDO也不支持。(但MySQLi支持它)
首先,让我们从为大家提供的标准注释开始:
请不要
mysql_*
在新代码中使用函数。它们不再维护,已正式弃用。看到红色框了吗?改为了解准备好的语句,并使用 PDO或库MySQLi - 本文将帮助你决定哪些。如果您选择PDO,这是一个很好的教程。
让我们逐句讲解,并解释一下:
它们已不再维护,并已正式弃用
这意味着PHP社区正在逐渐放弃对这些非常老的功能的支持。它们可能在将来的PHP版本中不存在!继续使用这些功能可能会在不远的将来破坏您的代码。
新!- 自PHP 5.5起, ext / mysql现在正式被弃用!
相反,您应该学习准备好的语句
mysql_*
扩展不支持准备好的语句,这是(除其他外)针对SQL Injection的非常有效的对策。它修复了依赖MySQL的应用程序中的一个非常严重的漏洞,攻击者可以利用该漏洞来访问您的脚本并在数据库上执行任何可能的查询。
有关更多信息,请参见如何防止PHP中的SQL注入?
看到红框了吗?
当您转到任何mysql
功能手册页面时,都会看到一个红色框,说明不再应该使用它。
使用PDO或MySQLi
有更好,更健壮和完善的替代方案:PDO-PHP数据库对象(提供了完整的OOP方法来进行数据库交互)和MySQLi(这是MySQL的特定改进)。
IN (...) construct
。
已经提到了分析和综合原因。对于新手来说,有一个更重要的动机是停止使用过时的mysql_函数。
当代数据库API 更加易于使用。
主要是可以简化代码的绑定参数。而且,通过出色的教程(如上所示),向PDO的过渡并不困难。
但是,立即重写较大的代码库需要花费时间。Raison d'être这个中间替代方案:
使用< pdo_mysql.php >,您可以毫不费力地从旧的mysql_函数切换。它添加了pdo_
功能包装器,以替换其mysql_
对应的包装器。
只需在每个必须与数据库进行交互的调用脚本中即可。
include_once(
"pdo_mysql.php"
);
删除函数前缀,并替换为mysql_
pdo_
。
mysql_
connect()
变成 pdo_
connect()
mysql_
query()
变成 pdo_
query()
mysql_
num_rows()
变成 pdo_
num_rows()
mysql_
insert_id()
变成 pdo_
insert_id()
mysql_
fetch_array()
变成 pdo_
fetch_array()
mysql_
fetch_assoc()
变成 pdo_
fetch_assoc()
mysql_
real_escape_string()
变成 pdo_
real_escape_string()
您的代码将以相似的方式工作,并且仍然大致相同:
include_once("pdo_mysql.php");
pdo_connect("localhost", "usrABC", "pw1234567");
pdo_select_db("test");
$result = pdo_query("SELECT title, html FROM pages");
while ($row = pdo_fetch_assoc($result)) {
print "$row[title] - $row[html]";
}
等等。
您的代码正在使用 PDO。
现在是时候实际使用它了。
您只需要一个不太麻烦的API。
pdo_query()
添加了对绑定参数的非常方便的支持。转换旧代码很简单:
将变量移出SQL字符串。
pdo_query()
。?
作为占位符放置在变量之前。'
以前包含字符串值/变量的单引号。对于更长的代码,此优势变得更加明显。
通常,字符串变量不仅会插值到SQL中,而且还会在两者之间转义调用。
pdo_query("SELECT id, links, html, title, user, date FROM articles
WHERE title='" . pdo_real_escape_string($title) . "' OR id='".
pdo_real_escape_string($title) . "' AND user <> '" .
pdo_real_escape_string($root) . "' ORDER BY date")
使用?
占位符后,您就不必再为此担心了:
pdo_query("SELECT id, links, html, title, user, date FROM articles
WHERE title=? OR id=? AND user<>? ORDER BY date", $title, $id, $root)
请记住,pdo_ *仍然允许或。
只是不要转义变量并将其绑定到同一查询中。
:named
占位符列表。更重要的是,您可以在任何查询后安全地传递$ _REQUEST []变量。当提交的<form>
字段与数据库结构完全匹配时,它会更短:
pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);
非常简单。但是,让我们回到一些更多的重写建议和技术原因上,了解为什么您可能想要摆脱和逃脱。mysql_
sanitize()
功能将所有调用转换为mysql_
pdo_query
带有绑定参数的pdo_real_escape_string
电话后,请删除所有多余的电话。
特别是,您应该以一种形式或另一种形式修复过时的教程中广告的任何sanitize
或clean
或filterThis
或clean_data
功能:
function sanitize($str) {
return trim(strip_tags(htmlentities(pdo_real_escape_string($str))));
}
这里最明显的错误是缺少文档。更重要的是,过滤顺序完全错误。
正确的顺序应该是:不建议将其stripslashes
作为最内层的调用,然后不建议将其作为输出上下文的使用,trim
其后strip_tags
,则应htmlentities
仅将_escape_string
其作为应用程序应直接在SQL插入之前进行。
但是,第一步就是摆脱_real_escape_string
通话。
sanitize()
如果您的数据库和应用程序流需要HTML上下文安全的字符串,则可能必须暂时保留其余功能。添加一条注释,该注释仅适用于以后的HTML转义。
字符串/值处理委托给PDO及其参数化语句。
如果stripslashes()
您的清理功能中提到任何内容,则可能表明存在更高级别的监督。
通常,这样做是为了消除已过时的损坏(两次逃逸)magic_quotes
。然而,最好是集中固定,而不是一串一串地固定。
使用userland逆转方法之一。然后删除stripslashes()
该sanitize
功能。
关于magic_quotes的历史性注释。该功能已被正确弃用。但是,通常将它错误地描述为失败的安全功能。但是,magic_quotes是一个失败的安全功能,就像网球作为营养源失败一样。那根本不是他们的目的。
PHP2 / FI中的原始实现仅通过“ 引号将被自动转义,从而使将表单数据直接传递到msql查询变得更加容易 ”而明确引入了它。值得注意的是,与mSQL一起使用是偶然安全的,因为它仅支持ASCII。
然后PHP3 / Zend为MySQL重新引入了magic_quotes并错误地记录了它。但是最初它只是一种便利功能,并不旨在保证安全性。
当您将字符串变量加扰到SQL查询中时,它不仅会使您更复杂。MySQL再次分离代码和数据也是多余的工作。
SQL注入只是什么时候 数据渗入代码上下文时发生。数据库服务器以后无法发现PHP最初将变量粘贴在查询子句之间的位置。
使用绑定的参数,可以在PHP代码中分隔SQL代码和SQL上下文值。但是它不会在后台再次被洗掉(PDO :: EMULATE_PREPARES除外)。您的数据库接收不变的SQL命令和1:1可变值。
尽管此答案强调您应该关注删除的可读性优点 。由于这种可见的和技术上的数据/代码分离,有时还具有性能优势(重复的INSERT具有不同的值)。mysql_
请注意,参数绑定仍然不是一个神奇的一站式解决方案 所有 SQL注入。它处理数据/值的最常见用法。但是不能将列名/表标识符列入白名单,不能帮助动态子句构造,或仅将简单数组值列表列入白名单。
这些pdo_*
包装器函数构成了易于编码的Stop-gap API。(MYSQLI
如果不是特殊功能签名转换,这几乎是本来可以的)。他们在大多数时候还公开真实的PDO。
重写并不仅限于使用新的pdo_函数名称。您可以将每个pdo_query()逐个转换为普通的$ pdo-> prepare()-> execute()调用。
最好还是从简化开始。例如,常见的结果获取:
$result = pdo_query("SELECT * FROM tbl");
while ($row = pdo_fetch_assoc($result)) {
可以用一个foreach迭代代替:
foreach ($result as $row) {
或者更好的是直接和完整的数组检索:
$result->fetchAll();
在大多数情况下,您将获得比查询失败后通常提供的PDO或mysql_更有用的警告。
因此,这有希望地显示出一些实际原因以及一条值得放弃的途径。mysql_
只需切换到 do并没有削减。pdo_query()
也是它的前端。
除非您还引入参数绑定或可以使用更好的API中的其他功能,否则这是毫无意义的选择。我希望它描绘得足够简单,以免让新来者感到沮丧。(教育通常比禁止做得更好。)
尽管它符合可以工作的最简单类别的要求,但它仍然是非常试验性的代码。我只是在周末写的。但是,还有很多其他选择。只是谷歌为PHP数据库抽象和浏览一点。一直存在并且将有许多出色的库来执行此类任务。
如果您想进一步简化数据库交互,那么像Paris / Idiorm这样的映射器值得一试。就像没有人在JavaScript中使用平淡的DOM一样,如今,您不必再忍受原始数据库接口。
pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);
功能-即:pdo_query("INSERT INTO users VALUES (?, ?, ?), $_POST); $_POST = array( 'username' => 'lawl', 'password' => '123', 'is_admin' => 'true');
pdo_real_escape_string()
<-这甚至是一个真正的功能,我找不到它的任何文档吗?请为此发布信息。
该mysql_
功能:
mysqli_
mysql_*
函数是mysqlnd函数的外壳,用于更新的PHP版本。因此,即使不再维护旧的客户端库,也将维护mysqlnd :)
说到技术原因,只有少数几个非常具体且很少使用。您很可能永远都不会在生活中使用它们。
也许我太无知了,但是我从来没有机会使用它们,例如
如果您需要它们-这些无疑是从mysql扩展转向更时尚和现代外观的技术原因。
但是,还有一些非技术性的问题,可能会使您的体验更困难
后一个问题是一个问题。
但是,我认为,提议的解决方案也不是更好。
在我看来,所有那些PHP用户都将学习如何立即正确处理SQL查询实在是太理想化了。最有可能的是,他们只是将mysql_ *机械地更改为mysqli_ *,而使方法相同。尤其是因为mysqli使准备好的语句用法难以置信的痛苦和麻烦。
更不用说本地预备语句不足以防止 SQL注入,而且mysqli和PDO都不提供解决方案。
因此,我宁愿与错误的做法作斗争并以正确的方式教育人们,而不是与诚实的扩展作斗争。
此外,还有一些错误或不重要的原因,例如
mysql_query("CALL my_proc");
了很长时间)最后一点很有趣。尽管mysql ext不支持本机预处理语句,但出于安全考虑,它们不是必需的。我们可以使用手动处理的占位符轻松伪造准备好的语句(就像PDO一样):
function paraQuery()
{
$args = func_get_args();
$query = array_shift($args);
$query = str_replace("%s","'%s'",$query);
foreach ($args as $key => $val)
{
$args[$key] = mysql_real_escape_string($val);
}
$query = vsprintf($query, $args);
$result = mysql_query($query);
if (!$result)
{
throw new Exception(mysql_error()." [$query]");
}
return $result;
}
$query = "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result = paraQuery($query, $a, "%$b%", $limit);
瞧,一切都已参数化且安全。
但是好吧,如果您不喜欢手册中的红色框,则会出现选择问题:mysqli或PDO?
好吧,答案如下:
如果像绝大多数PHP人士一样,您在应用程序代码中使用原始API调用(这实际上是错误的做法)-PDO是唯一的选择,因为此扩展伪装成不仅是API,而是半DAL,仍不完整,但提供了许多重要功能,其中两个使PDO与mysqli形成了鲜明的区别:
因此,如果您是PHP的普通用户,并且希望在使用本机准备好的语句时省去很多麻烦,那么PDO(再次)是唯一的选择。
但是,PDO也不是灵丹妙药,它有很多困难。
因此,我在PDO标签Wiki中针对所有常见陷阱和复杂案例编写了解决方案
但是,每个谈论扩展的人都始终缺少关于Mysqli和PDO 的两个重要事实:
准备好的声明不是万灵丹。有些动态标识符无法使用准备好的语句进行绑定。有些动态查询带有未知数量的参数,这使查询构建变得困难。
mysqli_ *和PDO函数均不应出现在应用程序代码中。它们和应用程序代码之间
应该有一个抽象层,它将完成内部的绑定,循环,错误处理等所有肮脏的工作,从而使应用程序代码变得干燥而干净。特别是对于复杂的情况,例如动态查询构建。
因此,仅切换到PDO或mysqli是不够的。必须使用ORM,查询构建器或任何数据库抽象类,而不是在其代码中调用原始API函数。
相反,如果您的应用程序代码和mysql API之间有一个抽象层,则实际上使用哪个引擎并不重要。您可以使用mysql ext直到它被弃用,然后轻松地将您的抽象类重写到另一个引擎,同时保留所有应用程序代码。
以下是一些基于我的safemysql类的示例,以说明这种抽象类应如何:
$city_ids = array(1,2,3);
$cities = $db->getCol("SELECT name FROM cities WHERE is IN(?a)", $city_ids);
将这一行与PDO所需的代码量进行比较。
然后将原始Mysqli准备好的语句与所需的大量代码进行比较。请注意,错误处理,性能分析,查询日志记录已内置并正在运行。
$insert = array('name' => 'John', 'surname' => "O'Hara");
$db->query("INSERT INTO users SET ?u", $insert);
将每个单个字段名称重复六到十次-在所有这些众多的命名占位符,绑定和查询定义中,将其与常规PDO插入进行比较。
另一个例子:
$data = $db->getAll("SELECT * FROM goods ORDER BY ?n", $_GET['order']);
您几乎找不到PDO处理这种实际情况的示例。
这太罗word了,很可能不安全。
因此,再一次-您不仅应该关注原始驱动程序,还应该关注抽象类,它不仅对初学者手册中的愚蠢示例有用,而且可以解决实际问题。
mysql_*
使得漏洞很容易得到。由于PHP被许多新手使用,mysql_*
因此实际上在实践上是有害的,即使在理论上可以毫不费力地使用它。
everything is parameterized and safe
-可以将其参数化,但是您的函数未使用实际的预准备语句。
Not under active development
只弥补那些“ 0.01%”呢?如果您使用此静态功能构建某些东西,并在一年内更新mysql版本并使用无法正常工作的系统结束,我肯定会突然有很多人使用“ 0.01%”的代码。我会这样说,deprecated
并且not under active development
关系密切。您可以说没有“ [值得]理由”,但事实是,当在选项之间进行选择时,no active development
几乎和deprecated
我说的一样糟糕?
原因很多,但也许最重要的原因是那些功能鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止SQL注入攻击。
使用mysql_*
函数时,必须记住要通过以下命令运行用户提供的参数mysql_real_escape_string()
。如果您只忘记一个地方,或者碰巧只对部分输入进行转义,则数据库可能会受到攻击。
在PDO
或中使用准备好的语句mysqli
会使语句更容易出错。
因为(除其他原因外)要确保对输入数据进行清理要困难得多。如果使用参数化查询,就像使用PDO或mysqli一样,则可以完全避免这种风险。
例如,有人可以用作用"enhzflep); drop table users"
户名。旧的函数将允许每个查询执行多个语句,因此,诸如此类的臭虫可以删除整个表。
如果要使用mysqli的PDO,则用户名最终将为"enhzflep); drop table users"
。
看到 bobby-tables.com。
The old functions will allow executing of multiple statements per query
-不,他们不会。ext / mysql不能进行这种注入-PHP和MySQL可以进行这种注入的唯一方法是使用MySQLi和mysqli_multi_query()
函数时。ext / mysql和未转义的字符串可能进行的种类注入是诸如' OR '1' = '1
从数据库中提取原本不希望访问的数据之类的事情。在某些情况下,可以注入子查询,但是仍然无法以这种方式修改数据库。
编写此答案的目的是显示绕过编写欠佳的PHP用户验证代码有多么微不足道,如何(以及使用何种方式)进行这些攻击以及如何用安全的预备语句替换旧的MySQL函数-基本上,就是为什么StackOverflow用户(可能有很多销售代表)骚扰新用户,以提问题以改善其代码。
首先,请随时创建此测试mysql数据库(我已将其称为我的准备):
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>
乍一看似乎足够合法。
用户必须输入登录名和密码,对吗?
辉煌,不要输入以下内容:
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对象完成的,其次,它使用的是准备好的语句。现在,您要问什么预先准备好的陈述?这是一种在运行查询之前告诉数据库我们将要运行的查询的方式。在这种情况下,我们告诉数据库:“嗨,我要运行一个选择ID,用户ID并从表用户传递的选择语句,其中用户ID是变量,而传递也是变量。”
然后,在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...
这就是为什么在发布问题时大喊大叫的原因-因为人们可以看到即使尝试也可以绕过您的代码。请使用此问题和答案来改进您的代码,使其更安全并使用最新功能。
最后,这并不是说这是完美的代码。您还可以做很多事情来改进它,例如使用哈希密码,确保在将有意义的信息存储在数据库中时,不要以纯文本形式存储它,而是要进行多级验证-但是实际上,如果您只要将旧的易于注入的代码更改为此,就可以很好地编写良好的代码-而且您已经走了很长一段距离并且仍在阅读中,这一事实让我感到,希望您不仅会实现这种类型编写网站和应用程序时的代码,但您可能会出去研究我刚才提到的其他内容-等等。编写可能的最佳代码,而不是几乎无法发挥作用的最基本的代码。
mysql_*
本身并不是不安全的,但是它确实通过糟糕的教程和缺乏适当的语句prepare API来促进不安全的代码。
我发现以上答案确实很冗长,因此总结一下:
mysqli扩展具有许多优点,相对于mysql扩展,主要的增强之处在于:
- 面向对象的界面
- 对准备好的语句的支持
- 支持多条语句
- 支持交易
- 增强的调试功能
- 嵌入式服务器支持
资料来源:MySQLi概述
如以上答案所述,mysql的替代品是mysqli和PDO(PHP数据对象)。
MySQLi和PDO都是在PHP 5.0中引入的,而MySQL是在PHP 3.0之前引入的。需要注意的一点是,PHP5.x中包含MySQL,尽管在更高版本中已弃用。
可以mysql_*
使用mysqli或PDO 定义几乎所有函数。只需将它们包括在旧的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;
}