一、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)关闭连接和释放资源
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 扩展库的预编译处理技术
$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 相当于是一个数据库抽象层,不同数据库使用相同的方法名,解决数据库连接不统一的问题。
三、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() 方法之间即可。