为什么我不应该在 PHP 中使用 mysql_ * 函数?

为什么不应该使用mysql_*函数的技术原因是什么? (例如mysql_query()mysql_connect()mysql_real_escape_string() )?

即使我的网站上可以工作,我为什么还要使用其他东西?

如果它们在我的网站上不起作用,为什么会出现类似

警告:mysql_connect():没有这样的文件或目录

答案

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 扩展的比较

PHP 提供了三种不同的 API 连接到 MySQL。这些是mysql (从 PHP 7 开始删除), mysqliPDO扩展。

mysql_*函数曾经很流行,但是不再鼓励使用它们。文档团队正在讨论数据库安全状况,并且教育用户远离常用的 ext / mysql 扩展是其中一部分(请检查php.internals:不赞成使用 ext / mysql )。

后来的 PHP 开发人员团队决定,当用户连接到 MySQL 时,无论是通过mysql_connect()mysql_pconnect()还是ext/mysql内置的隐式连接功能,都会生成E_DEPRECATED错误。

ext/mysql 自 PHP 5.5正式弃用 ,自 PHP 7起已被删除

看到红框了吗?

当您进入任何mysql_*功能手册页面时,都会看到一个红色框,说明不再应该使用它。

为什么


离开ext/mysql不仅关系到安全性,而且关系到访问 MySQL 数据库的所有功能。

ext/mysql是为MySQL 3.23构建的,此后仅增加了很少的内容,同时主要保持了与该旧版本的兼容性,这使得代码难以维护。 ext/mysql不支持的缺少的功能包括:( 来自 PHP 手册 )。

不使用mysql_*函数的原因

  • 没有积极发展
  • 从 PHP 7 开始删除
  • 缺乏 OO 接口
  • 不支持非阻塞异步查询
  • 不支持预备语句或参数化查询
  • 不支持存储过程
  • 不支持多条语句
  • 不支持交易
  • 不支持 MySQL 5.1 中的所有功能

昆汀的答案引述以上观点

缺少对准备好的语句的支持尤其重要,因为与使用单独的函数调用手动转义相比,它们提供了一种更清晰,更易于出错的转义和引用外部数据的方法。

请参见SQL 扩展比较


禁止弃用警告

当代码被转换为MySQLi / PDO ,可以通过在php.ini E_DEPRECATED: error_reporting设置为排除E_DEPRECATED:来抑制E_DEPRECATED错误E_DEPRECATED:

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 (数据源名称)和可选的usernamepassword

在这里,我认为除了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会执行 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_*几乎相同,在mysql_* ,您必须检查每个结果,然后查看$db->errorInfo();获取错误详细信息。
  • PDO::ERRMODE_WARNING提高E_WARNING 。 (运行时警告(非致命错误)。不会停止执行脚本。)
  • PDO::ERRMODE_EXCEPTION :引发异常。它表示 PDO 引发的错误。您不应从自己的代码中引发PDOException 。有关 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()进行处理, or die()可以说像mysql_* ,但它确实会有所不同。您可以通过关闭display_errors off并阅读错误日志来隐藏生产中的危险错误消息。

现在,在阅读完上述所有内容之后,您可能会想:当我只想开始使用简单的SELECTINSERTUPDATEDELETE语句时,这到底是什么?不用担心,我们开始:


选择数据

PDO选择图片

因此,您在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 如何获取数据。在下面的部分中,我将对此进行解释。

提取模式

请注意,上面的fetch()fetchAll()代码中使用了PDO::FETCH_ASSOC 。这告诉PDO将行作为关联数组返回,字段名称作为键。我也将一一解释其他许多提取模式。

首先,我说明如何选择提取模式:

$stmt->fetch(PDO::FETCH_ASSOC)

在上面,我一直在使用fetch() 。您还可以使用:

现在,我进入获取模式:

  • 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();

插入和更新或删除语句

插入和更新PDO映像

我们在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 的三分之三 ):

  1. 准备 :语句模板由应用程序创建,并发送到数据库管理系统(DBMS)。某些值未指定,称为参数,占位符或绑定变量(在下面标记为? ):

    INSERT INTO PRODUCT (name, price) VALUES (?, ?)

  2. DBMS 对语句模板进行解析,编译和查询优化,并在不执行结果的情况下存储结果。

  3. 执行 :稍后,应用程序提供(或绑定)参数值,然后 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()方法的数组中元素的正确顺序。


SELECTINSERTUPDATEDELETE预备查询

  1. 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);
  2. INSERT

    $stmt = $db->prepare("INSERT INTO table(field1,field2) VALUES(:field1,:field2)");
    $stmt->execute(array(':field1' => $field1, ':field2' => $field2));
    $affected_rows = $stmt->rowCount();
  3. DELETE

    $stmt = $db->prepare("DELETE FROM table WHERE id=:id");
    $stmt->bindValue(':id', $id, PDO::PARAM_STR);
    $stmt->execute();
    $affected_rows = $stmt->rowCount();
  4. 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 /*"));

首先,让我们从为大家提供的标准注释开始:

请不要在新代码中使用mysql_*函数 。它们不再维护,已正式弃用 。看到红色框了吗?相反,要了解准备好的语句 ,并使用PDOMySQLi- 本文将帮助您确定哪一个。如果您选择 PDO, 这是一个很好的教程

让我们逐句讲解,并解释一下:

  • 它们已不再维护,并已正式弃用

    这意味着 PHP 社区正在逐渐放弃对这些非常老的功能的支持。它们可能在将来的 PHP 版本中不存在!继续使用这些功能可能会在不远的将来破坏您的代码。

    新! - 自 PHP 5.5 起, ext / mysql 现在正式被弃用!

    较新! ext / mysql 已在 PHP 7 中删除

  • 相反,您应该学习准备好的语句

    mysql_*扩展名不支持预处理语句 ,这是(除其他事项之外)针对SQL Injection的非常有效的对策。它修复了依赖 MySQL 的应用程序中的一个非常严重的漏洞,攻击者可以利用该漏洞来访问您的脚本并在数据库上执行任何可能的查询

    有关更多信息,请参见如何防止 PHP 中的 SQL 注入?

  • 看到红框了吗?

    当您转到任何mysql功能手册页面时,都会看到一个红色框,说明不再使用它。

  • 使用 PDO 或 MySQLi

    有更好,更健壮和完善的替代方案: PDO-PHP 数据库对象 (提供了完整的 OOP 方法来进行数据库交互)和MySQLi (这是 MySQL 的特定改进)。

使用方便

已经提到了分析和综合原因。对于新手来说,有更大的动机停止使用过时的 mysql_函数。

当代数据库 API 更加易于使用。

主要是可以简化代码的绑定参数 。而且,通过出色的教程(如上所示) ,向PDO的过渡并不困难。

但是,立即重写较大的代码库需要花费时间。 Raison d'être 这个中间替代方案:

等效的 pdo_ * 函数代替mysql_ *

使用您就可以用最小的努力旧 mysql_功能切换。它添加了pdo_函数包装,以替换它们的mysql_对应对象。

  1. 只需include_once( "pdo_mysql.php" );在每个必须与数据库进行交互的调用脚本中。

  2. mysql_ 地方删除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()
    • 等等...

  3. 您的代码将以相似的方式工作,并且仍然大致相同:

    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_ * 仍然允许
只是不要转义变量并将其绑定到同一查询中。

  • 占位符功能由其后面的实际 PDO 提供。
  • 因此,以后也允许:named占位符列表。

更重要的是,您可以在任何查询后安全地传递 $ _REQUEST [] 变量。当提交的<form>字段与数据库结构完全匹配时,它甚至更短:

pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);

非常简单。但是,让我们回到一些重写建议和技术原因上,以了解为什么您可能想要摆脱mysql_和转义。

修复或删除任何老式的sanitize()函数

将所有mysql_调用转换为带有绑定参数的pdo_query ,请删除所有冗余的pdo_real_escape_string调用。

特别是,您应该按照日期指南以一种或另一种形式修复任何sanitizecleanfilterThisclean_data函数:

function sanitize($str) {
   return trim(strip_tags(htmlentities(pdo_real_escape_string($str))));
}

这里最明显的错误是缺少文档。更重要的是,过滤顺序完全错误。

  • 正确的顺序应该是:不建议将stripslashes作为最里面的调用,然后将trim ,之后strip_tags ,用于输出上下文的htmlentities ,以及最后_escape_string因为它的应用程序应直接在 SQL 进行_escape_string

  • 但第一步是摆脱_real_escape_string调用。

  • 如果您的数据库和应用程序流需要 HTML 上下文安全的字符串,则可能必须暂时保留其余的sanitize()函数。添加一条注释,该注释仅适用于以后的 HTML 转义。

  • 字符串 / 值处理委托给 PDO 及其参数化语句。

  • 如果您的清理功能中提到了stripslashes() ,则可能表明存在更高级别的监督。

    关于 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 可变值。

尽管此答案强调您应该关注删除mysql_的可读性优点。由于这种可见的和技术上的数据 / 代码分离,有时还具有性能优势(重复的 INSERT 具有不同的值)。

注意,对于所有 SQL 注入,参数绑定仍然不是一个神奇的一站式解决方案。它处理数据 / 值的最常见用法。但是不能将列名 / 表标识符列入白名单,不能帮助动态子句构造,或仅将简单数组值列表列入白名单。

混合 PDO 使用

这些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_途径。

仅仅切换到并不能完全消除它。 pdo_query()也是它的前端。

除非您还引入参数绑定或可以使用更好的 API 中的其他功能,否则这是毫无意义的选择。我希望它描绘得足够简单,以免让新来者感到沮丧。 (教育通常比禁止做得更好。)

尽管它符合可以工作的最简单类别的要求,但它仍然是非常试验性的代码。我只是在周末写的。但是,还有很多其他选择。只是谷歌为PHP 数据库抽象和浏览一点。一直存在并且将有许多出色的库来执行此类任务。

如果您想进一步简化数据库交互,那么像Paris / Idiorm这样的映射器值得一试。就像没有人在 JavaScript 中使用平淡的 DOM 一样,如今,您不必再忍受原始数据库接口。

mysql_函数:

  1. 已过时 - 已不再维护
  2. 不允许您轻松移动到另一个数据库后端
  3. 不支持准备好的语句,因此
  4. 鼓励程序员使用串联来构建查询,从而导致 SQL 注入漏洞

说到技术原因,只有少数几个非常具体且很少使用。您很可能永远都不会在生活中使用它们。
也许我太无知了,但是我从来没有机会使用它们,例如

  • 非阻塞异步查询
  • 存储过程返回多个结果集
  • 加密(SSL)
  • 压缩

如果您需要它们 - 这些无疑是从 mysql 扩展转向更时尚和现代外观的技术原因。

但是,还有一些非技术性的问题,可能会使您的体验更困难

  • 在现代 PHP 版本中进一步使用这些功能将引起不推荐使用的通知。只需将其关闭即可。
  • 在不久的将来,可以将它们从默认的 PHP 版本中删除。没什么大不了的,因为 mydsql ext 将被转移到 PECL 中,并且每个托管者都乐于用它编译 PHP,因为他们不想失去那些使用了数十年站点的客户端。
  • 来自 Stackoverflow 社区的强烈抵抗。每次您提到这些诚实的功能时,都会被告知这些功能是严格的禁忌。
  • 作为一名普通的 PHP 用户,您使用这些功能的想法很容易出错和出错。正是由于所有这些无数的教程和手册都教给您错误的方法。不是函数本身 - 我必须强调它 - 而是函数的使用方式。

后一个问题是一个问题。
但是,我认为,提议的解决方案也不是更好。
在我看来,所有那些 PHP 用户都将学习如何立即正确处理 SQL 查询实在是太理想化了。最有可能的是,他们只是将 mysql_ * 机械地更改为 mysqli_ *, 而使方法相同 。尤其是因为 mysqli 使准备好的语句用法难以置信的痛苦和麻烦。
更不用说本地预备语句不足以防止 SQL 注入,而且 mysqli 和 PDO 都不提供解决方案。

因此,我宁愿与错误的做法作斗争并以正确的方式教育人们,而不是与诚实的扩展作斗争。

此外,还有一些错误或不重要的原因,例如

  • 不支持存储过程(我们使用mysql_query("CALL my_proc");很久了)
  • 不支持交易(与上述相同)
  • 不支持多条语句(谁需要它们?)
  • 不在积极的开发下(那又是什么呢?它会以任何实际的方式影响吗?)
  • 缺少 OO 接口(创建一个 OO 接口大约需要几个小时)
  • 不支持预备语句或参数化查询

最后一点很有趣。尽管 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?

好吧,答案如下:

  • 如果您了解使用数据库抽象层并寻找创建一个 API 的必要性,那么mysqli是一个很好的选择,因为它确实支持许多特定于 mysql 的功能。
  • 如果像绝大多数 PHP 专家一样,您在应用程序代码中使用原始 API 调用(这实际上是错误的做法) -PDO 是唯一的选择 ,因为此扩展伪装成不仅是 API,而是半 DAL,仍不完整,但提供了许多重要功能,其中两个使 PDO 与 mysqli 形成了鲜明的区别:

    • 与 mysqli 不同,PDO 可以通过 value绑定占位符,这使得动态构建的查询变得可行,而无需几个混乱的屏幕。
    • 与 mysqli 不同,PDO 始终可以以简单的常规数组返回查询结果,而 mysqli 只能在 mysqlnd 安装中执行此操作。

因此,如果您是 PHP 的普通用户,并且希望在使用本机准备好的语句时省去很多麻烦,那么 PDO(再次)是唯一的选择。
但是,PDO 也不是灵丹妙药,它有很多困难。
因此,我在PDO 标签 Wiki 中针对所有常见陷阱和复杂案例编写了解决方案

但是,每个谈论扩展的人都始终缺少关于 Mysqli 和 PDO 的两个重要事实

  1. 准备好的声明不是万灵丹 。有些动态标识符无法使用准备好的语句进行绑定。有些动态查询带有未知数量的参数,这使查询构建变得困难。

  2. 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 了,很可能不安全。

因此,再次 - 您不仅要关注原始驱动程序,还应该关注抽象类,它不仅对初学者手册中的愚蠢示例有用,而且可以解决实际问题。

原因很多,但也许最重要的原因是那些功能鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止 SQL 注入攻击。

使用mysql_*函数时,必须记住要通过mysql_real_escape_string()运行用户提供的参数。如果您只忘记一个地方,或者碰巧只对部分输入进行转义,则数据库可能会受到攻击。

PDOmysqli使用准备好的语句将使它变得更加难以编写。

因为(除其他原因外)要确保对输入数据进行清理要困难得多。如果使用参数化查询,就像使用 PDO 或 mysqli 一样,则可以完全避免这种风险。

例如,有人可以使用"enhzflep); drop table users"作为用户名。旧的函数将允许每个查询执行多个语句,因此,诸如此类的臭虫可以删除整个表。

如果要使用 mysqli 的 PDO,则用户名最终将是"enhzflep); drop table users"

请参阅bobby-tables.com

编写此答案的目的是显示绕过编写不良的 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 扩展是这三个中最老的扩展,是开发人员用来与 MySQL 通信的原始方式。由于 PHP 和 MySQL 的新版本中进行了改进,因此不建议使用此扩展,而推荐使用其他两个 替代方法

  • MySQLi是使用 MySQL 数据库的 “改进” 扩展。它利用了更新版本的 MySQL 服务器中可用的功能,向开发人员公开了面向函数和面向对象的接口,并且还做了其他一些漂亮的事情。

  • PDO提供了一个 API,该 API 整合了以前分散在主要数据库访问扩展(例如 MySQL,PostgreSQL,SQLite,MSSQL 等)中的大多数功能。该接口公开了高级对象,供程序员使用数据库连接,查询和结果集和低级驱动程序与数据库服务器执行通信和资源处理。 PDO 进行了大量讨论和工作,它被认为是使用现代专业代码处理数据库的适当方法。