PHP新特性 第8章 PDO PHP新特性 第8章 PDO

2016-07-10

一、php 的 sql 注入攻击

1.1、什么是 sql 注入攻击

用户提交一段数据库查询代码,根据返回的结果,获得某些他想得到的数据。

比如 :查询某个管理员是否存在,一般程序员会这么写

$sql = "select * from user where name='luluyii' and password='****'";
if (查询到) {
     header("admin.php");
}else {
     header("login.php");
}

这样写,容易被人通过 sql 注入攻击进入你的 admin.php 页面。

1.2、万能密码和万能用户名

(1)方式一

① 万能密码

$sql = "select * from user where name='**' and password='***' or 1='1'";

即万能密码是:**' or 1='1

② 万能用户名

$sql = "select * from user where name='**' union select * form user/* and password='***';

/*表示不执行后面的语句,即万能用户名是:**' union select * from user/*

(二)方式二

$sql = 去掉 '$name' 和 '$password'的单引号;

这种写法,没有' ',mysql数据库会把你的输入当数字对待

① 使用万能密码:数字 union select * form user

② 使用万能用户名:数字 union select * form user/*

1.3、防止用户登录注入

(1)在php.ini 中开启 magic_quotes_gpc = On,但是这个特性在 php 5.3.0 中已经废弃,并且在 php 5.4.0 中已经移除。

若配置了,方式一中的万能用户名和万能密码将失效,但是方式二仍然有效。

原因是:当改为 on 之后,Apache 服务器会对 ' 加入 \ 转义,如name = 'luluyii',当数据库执行时,变成 name = \'luluyii\'

(2)密码对比法

① 基本思想:改变验证数据库用户逻辑。

首先通过用户输入的用户名去查询数据库,如果查询到这个用户对应的密码,和用户提交的密码对比,相同则说明该用户合法,否则不合法。

$sql = "select password form user where name='luluyii'"; 
if(从数据库查询到的密码 == 用户输入的密码){ 
    header("admin.php"); 
}else{ 
    header("login.php"); 
}

分析:这样写,用户无法通过 password = 注入攻击,因为 if( == ) 是在php中验证的。

② 使用 pdo 的预编译来解决 sql 注入

首先在 php,ini 中启用 pdo:extension = php_pdo_mysql.dll

$mypdo = new PDO("mysql:localhost;port=3306;dbname=luluyii","root","root");
$pdoStatment = $mypdo->prepare($sql); //预编译
$pdoStatment->execute(array($name,$password)); //接收
$pdoStatment->fetch(); //取出结果

1.4、php 搜索引擎中 sql 注入问题

(1)一般使用 _ _ 和 % 攻击,获取所有数据

(2)防止查询 sql 攻击

$keyWord = addslashes($keyWord); //使用反斜杠引用字符串
$keyWord = str_replace("%","\%",$keyWord); //将 % 替换为 \%
$keyWord = str_replace("_","\_",$keyWord); //将 _ 替换为 \_

1.5、总结

防止 SQL 注射漏洞一般用什么函数?

addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。
$str = addslashes('Shanghai is the "biggest" city in China.');
echo($str);
//输出 Shanghai is the \"biggest\" city in China.

sql 注入的方式还有其它很多形式,我们要写出健壮安全的代码,就要不断提高编写安全代码的意识,让我们的代码更符合

商业要求。

二、php 数据库编程 mysqli、pdo

2.1、mysqli

2.1.1、mysql 和 mysqli 说明

(1)msyqli 是 mysql 扩展库的增强版

(2)mysql 和 mysqli的比较

① mysqli 的稳定性、安全性、效率都有所提升

② mysqli 支持面向对象编程,同时考虑到 php 老程序员,提供面向过程的编程风格

2.1.2、案例

$conn = mysqli_connect("localhost",'root','root');
if (!$conn){
     die("数据库连接失败".mysqli_error());
}
mysqli_select_db($conn,"luluyii");
mysqli_query($conn,"set names utf8");
$sql = "select * from lulu_user";
$res = mysqli_query($conn,$sql);
while($row = mysqli_fetch_row($res)){
     foreach ($row as $key=>$val){
         echo "$val--";
     }
     echo "
";
}
mysqli_free_result($res);
mysqli_close($conn);

2.1.3、细节说明

(1)关闭连接和释放资源

https://file.lulublog.cn/images/3/2022/08/wgT42NeO2ZhXxTeDSSt4GnhgToNoeo.png

mysqli_close($conn); //关闭连接,是把 ① 断开;
mysqli_free_result($res); //释放资源,是把 ② 断开。

(2)从 $res 获取行数据的四个方法

① mysqli_fetch_row 返回一个索引数组
② mysqli_fetch_assoc 返回一个关联数组
③ mysqli_fetch_array 返回索引和关联数组
④ mysqli_fetch_object 把一行数据当做对象返回

(3)函数

mysqli_insert_id($conn)  取得上一步 Insert 操纵产生的 ID
$field_info = mysqli_fetch_field($res) 返回包含字段信息的对象,如$field_info->name
mysqli_affected_rows($conn) 取得前一次mysql操作所影响的记录行数

(4)三种释放 $res 结果集的方法

① $res->free(); ② $res->close(); ③ $res->free_result();

2.1.4、批量执行sql 语句

ddl 数据定义语句:CREATE TABLE

dml 数据操作语句:update、insert、delete

dql 数据查询语句:select

dcl 数据事务语句:rollback、commit

(1)批量执行 dml 语句

$sqls = "sql1;sql2;...";
$mysqli->multi_query($sqls);

(2)批量执行 dql 语句

$mysqli->store_result(); //从 mysqli 连接取出一个结果集
$mysqli->fetch_row();
$mysqli->more_results(); //用于判断是否有新的结果集
$mysqli->next_result(); //用于指向下一个结果集,但它不会判断下一个结果集是否存在,故需使用 $mysqli->more_results()

2.2、事务控制

2.2.1、概念

事务用于保证数据的一致性,它由一组相关的 dml 语句组成,该组的 dml 语句要么全部成功,要么全部失败。

如:网上转账就是典型的要用事务来处理,用以保证数据的一致性。

2.2.2、事务的 ACID 性质

① Atomicity 原子性:操作不可分割,要么都发生,要么都不发生

② Consistency 一致性:是一个数据库从一个一致性状态到另一个一致性状态

③ Isolation 隔离性:一旦开始事务,别的操作无法使用你正在使用的数据库

④ Durability 持久性:一旦提交,数据库的状态不再发生变化

2.2.3、在 mysqli 控制台可以使用事务来操作

① start transaction 开启一个事务

② savepoint a 保存点

③ 操作

④ 可回滚,可提交:若无问题 —— commit 提交,若有问题 —— rollback to a 回滚到 a

2.3、mysqli 扩展库的预编译处理技术

https://file.lulublog.cn/images/3/2022/08/Cb7Rc4FHoNsFb7o33b3mMB4fBH7fNB.png

$sql = "insert into user (name,password,email,age) values(?,?,?,?)";

这里的 ? 是个占位符,告诉数据库无需编译,只是数据的变化。

优点:效率高,执行速度快;安全性高,防止 sql 注入。

2.4、PDO —— PHP Data Object

2.4.1、基本介绍

① 该扩展在 PHP 5 中加入

② PHP 6 中默认使用 PDO 连接数据库

2.4.2、什么是 PDO

PDO 相当于是一个数据库抽象层,不同数据库使用相同的方法名,解决数据库连接不统一的问题。

https://file.lulublog.cn/images/3/2022/08/V4441mqbAYBAAZfbMZOaONtQBeN8Qm.png

三、PDO

PHP 支持多种数据库,如 MySQL、PostgreSQL、SQLite 和 Oracle 等,并且这些数据库都提供了用于 PHP 和相应数据库之间通信的扩展,如 mysqli、sqlite3 等。这样造成的一个问题是如果项目中使用了多种数据库,需要安装并使用多种 PHP 数据库扩展和接口,增加了学习和维护的成本。为此,从 PHP 5.1 开始引入了一个新的扩展 —— PDO。

3.1、PDO 扩展

PDO(PHP Data Objects)是一系列 PHP 类,抽象了不同数据库的具体实现,只通过同一套接口就可以与不同的数据库通信,极大地降低了学习成本,同时提高开发效率。

注:尽管 PDO 为不同数据库提供了统一接口,但是仍然必须自己编写 SQL 语句,这是 PDO 的劣势所在。

这样存在的一个隐患是不同的数据库 SQL 语句语法可能略有出入,所以在切换数据系统的时候需要注意这一点,建议尽可能编写符合 ANSI/ISO 标准的 SQL 语句。

3.2、数据库连接和 DSN

PDO 的构造函数中有一个字符串参数,用于指定 DSN(Data Source Name)来提供数据库连接的详细信息:

try {
    $pdo = new PDO(
        'mysql:host=127.0.0.1;dbname=test;port=3306;charset=utf8',
        'username',
        'password'
    );
} catch (PDOException $ex) {
    echo 'database connection failed';
    exit();
}

可见 DSN 主要包含信息包括:数据库主机名/IP地址、端口号、数据库名称、字符集。

PDO 构造函数的第二个参数是数据库用户名,第三个参数是该用户名对应密码。

3.3、保证数据库凭证的安全

为了保证数据库凭证(用户名/密码)的安全,不能将其硬编码在代码中,尤其是可以公开访问的 PHP 文件。

我们应该将其存放在一个位于文档根目录之外的配置文件中,然后在需要使用凭证的地方引入,正如 Laravel 所实现的那样(用户名密码信息位于 .env,隔离在版本控制之外):

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

然后在 config/database.php 中引入:

'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),
    'unix_socket' => env('DB_SOCKET', ''),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
    'strict' => true,
    'engine' => null,
],

3.4、预处理语句

现在我们建立了一个连接到数据库的 PDO 实例,通过这个实例就可以使用 SQL 语句从数据库读取数据,或者将数据写入数据库。

开发 PHP 应用时,我们经常需要从 HTTP 请求中获取动态值来定制 SQL 语句,如使用 /user?name=lulublog 来显示具体账户的资料信息,对应的 SQL 语句可能是:

SELECT * FROM user WHERE name = "lulublog";

初级 PHP 开发者可能会像这样构建这个 SQL 语句:

$sql = sprintf(
    'SELECT * FROM user WHERE name = "%s"', 
    filter_input(INPUT_GET, 'name')
);

这样做就会有 SQL 注入隐患。所以,在基于用户请求参数构建 SQL 语句时,一定要过滤用户输入参数值。幸运的是,在 PDO 中,我们可以通过预处理语句和参数绑定来实现用户输入过滤,从而避免 SQL 注入。

预处理语句是一个 PDOStatement 实例,我们可以通过 prepare() 方法来返回该实例:

$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);

预处理语句会自动过滤 $name 的值,防止数据库遭受 SQL 注入攻击。

3.5、查询结果

有了预处理语句之后,就可以在数据库中执行 SQL 查询了,调用预处理语句的 execute() 方法后就会使用绑定的所有数据执行 SQL 语句,如果执行的是 INSERT、UPDATE 或 DELETE 语句,执行完 execute() 方法工作结束了,如果执行的是 SELECT 语句,我们还期望数据库能返回匹配的结果。我们可以使用以下方法获取查询结果:

  • fetch()

  • fetchAll()

  • fetchColumn()

  • fetchObject()

fetch() 方法用于获取结果集的下一行,我们可以使用这个方法迭代大型结果集:

$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();

while (($result = $statement->fetch(PDO::FETCH_ASSOC)) !== FALSE) {
    echo $result['name'];
}

我们在调用 fetch() 方法时,传入了 PDO::FETCH_ASSOC 参数,该参数决定如何返回查询结果,该参数支持以下常量:

  • PDO::FETCH_ASSOC:返回关联数组,数组的键是数据表的列名

  • PDO::FETCH_NUM:返回键为数字的数组

  • PDO::FETCH_BOTH:顾名思义,返回一个既有键为列名又有键为数字的数组

  • PDO::FETCH_OBJ:返回一个对象,对象的属性是数据表的列名

如果处理的小型结果集合,可以使用 fetchAll() 方法获取所有查询结果,Laravel 框架底层的 select() 方法中就使用了该方法来获取返回结果集,所以在获取大量结果时不能使用该方法:

$sql = 'SELECT * FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();

$results = $statement->fetchAll(PDO::FETCH_ASSOC);
if ($results) {
    foreach ($results as $result) {
        echo $result['name'];
    }
}

如果只关心查询结果中的一列,可以使用 fetchColumn() 方法:

$sql = 'SELECT id, name FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();

while (($name = $statement->fetchColumn(1)) !== FALSE) {
    echo $name;
}

我们还可以使用 fetchObject() 方法获取查询结果中的行,这个方法把行当做对象:

$sql = 'SELECT id, name FROM user WHERE name = :name';
$statement = $pdo->prepare($sql);
$name = filter_input(INPUT_GET, 'name');
$statement->bindValue(':name', $name, PDO::PARAM_STR);
$statement->execute();

while (($result = $statement->fetchObject()) !== FALSE) {
    echo $result->name;
}

3.6、事务

事务是把一系列数据库操作当作一个逻辑单元执行,也就是说,事务中的一系列 SQL 语句要么都执行成功,要么都失败,事务的原子性能保证数据的一致性、安全性和持久性。

PDO 扩展中使用事务很容易,只需把想要执行的 SQL 语句放在 PDO 实例的 beginTransaction() 方法和 commit() 方法之间即可。

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开微信扫一扫,即可进行扫码打赏哦

阅读 2973