基础概念
预处理 SQL
很多情况下,某一条 SQL 语句可能会被反复调用执行,语句格式一样,只是值不同,如果每次都需要经过分析查询得到相同的编译结果,这样效率就很低
预编译语句就是将此类 SQL 语句中的值用占位符替代,可以视为将 SQL 语句模板化或者说参数化,实现一次编译、多次运行,省去了多次分析查询的过程
预处理的优势
- 效率高,执行速度快
- 安全性高,可以防止sql注入
预处理分析图
插入一个记录需要的时间由下列因素组成,其中的数字表示大约比例:
连接:(3)
发送查询给服务器:(2)
分析查询:(2)
插入记录:(1x记录大小)
插入索引:(1x索引)
关闭:(1)
普通流程
连接,发送sql并接收,分析查询(如果出错,返回错误信息,如果没错,继续往下走),插入操作(入库),如果有索引,还要继续维护索引,关闭连接
sql语句是在dbms中分析查询的,php是看不懂的,所以即使php中的sql语句写错,也不会报错
预处理流程
在执行sql语句的时候,首先将前面相同的命令和结构部分发送给MySQL服务器,让MySQL服务器事先进行一次预处理(此时并没有真正的执行sql语句),而为了保证sql语句的结构完整性,在第一次发送sql语句的时候将其中可变的数据部分都用一个数据占位符来表示,第二次只需要发送 参数 即可,实现一次编译、多次运行
图不好画,发送的应该是参数,重点就是预处理后会跳过分析查询这一步,从而提升效率
预处理示例(面向对象)
预处理 dml
//1.连接数据库 创建mysqli对象
$conn = new mysqli('localhost','root','','test');
if ($conn->connect_errno){
die('连接失败'.$conn->connect_error);
}else{
echo '连接成功';
}
$conn->query('set names utf8');
//2.创建预处理对象
//可以只存部分字段 前提是其他字段有默认值 ? 占位
//$sql = 'insert into user (id,name,$password,$email,$age))values (?,?,?,?,?)';
$sql = 'insert into user values (?,?,?,?,?)';
$stmt = $conn->prepare($sql) or die($conn->error);
//3.绑定参数 (就是给 ? 赋值)
//mysqli_stmt_bind_param()函数把变量绑定给预处理对象
//isdb: int str double/floot bool
//这里类型和顺序都要对应
//疑问:php中变量不可以先使用后定义吧? 个人猜测既然这里是引用赋值 那么肯定是有相关的全局变量的 就不不知怎么来的
$stmt->bind_param('isssi',$id,$name,$password,$email,$age);
//====================================================================
//4.设置参数数据
$id = 3;
$name = 'ww';
//这里不能用函数 如 md5(123)
$password = '123';
$email = 'ww@qq.com';
$age = 18;
//5.执行一个编译过的语句
//execute()函数返回值为 bool
//如果这里添加失败 程序仍然会向下执行 添加下一条数据 感觉每条数据都是独立的
//你可以手动进行错误控制,结束程序,也可以写成错误日志,然后继续执行
//$stmt->execute() or die('执行失败'.$stmt->error);
if (!$stmt->execute()){
error_log($id.'没加进去',3,'myerror.txt');
}
//======================================================================
//4.设置参数数据(只需要绑定一次参数就行)
$id = 4;
$name = 'kk';
$password = '123';
$email = 'kk@qq.com';
$age = 28;
//5.执行一个编译过的语句
if (!$stmt->execute()){
error_log($id.'没加进去',3,'myerror.txt');
}
//=======================================================================
//6.关闭连接
//关闭预编译
$stmt->close();
//关闭连接
$conn->close();
预处理 dql
//1.连接数据库 创建mysqli对象
$conn = new mysqli('localhost','root','','test');
if ($conn->connect_errno){
die('连接失败'.$conn->connect_error);
}else{
echo '连接成功';
}
$conn->query('set names utf8');
//2.创建预处理对象
$sql = 'select * from user where id>?';
$stmt = $conn->prepare($sql);
//3.绑定参数 (就是给 ? 赋值)
$stmt->bind_param('i',$id);
//4.设置参数数据
$id = 2;
//5.绑定结果集
//注意 这里是引用传递 这里的参数 与 sql语句 value参数对应
$stmt->bind_result($id,$name,$password,$email,$age);
//6.执行一个编译过的语句
if ($stmt->execute()){
echo '查询成功';
//7.取结果
//这里不需要$row=$stmt->fetch() 直接执行该方法会把值对应赋给变量,因为上面是引用传递,所以值可变
while ($stmt->fetch()){
echo'<br>';
echo $id.'--'.$name.'--'.$password.'--'.$email.'--'.$age;
}
}else{
echo '查询失败';
}
//8.释放结果集 关闭连接
//释放结果集(资源)
$stmt->free_result();
//关闭预编译
$stmt->close();
//关闭连接
$conn->close();
预处理防止sql注入
预编译可以自动防止 sql (结构化查询语句) 注入攻击使用万能密码
管理员登录,需要账户密码
注入案例
$conn = new mysqli('localhost','root','','test');
if ($conn->connect_errno){
die('连接失败'.$conn->connect_error);
}
$conn->query('set names utf8');
$sql = 'select * from users where id=100 and pwd = \'aaa\' or 1=\'1\'';
$res = $conn->query($sql);
if (!$res){
die('读取数据失败<br>'.$conn->error);
}else{
echo '读取数据成功<br>';
}
while ($row = $res->fetch_row()){
foreach ($row as $k=>$v){
echo "$v--";
}
echo '<br>';
}
$res->free();
$conn->close();
================================== cmd中演示 ===============================================
mysql> create table users(id int,name varchar(32),pwd varchar(32));
Query OK, 0 rows affected (0.08 sec)
mysql> insert into users values(100,'zy','123');
Query OK, 1 row affected (0.03 sec)
mysql> select * from users where id=100 and pwd = '123';
+------+------+------+
| id | name | pwd |
+------+------+------+
| 100 | zy | 123 |
+------+------+------+
1 row in set (0.00 sec)
//账号 100 密码 xx 密码错误
mysql> select * from users where id=100 and pwd = 'xx';
Empty set (0.00 sec)
//账号 100 密码 xx' or 1='1 万能密码
mysql> select * from users where id=100 and pwd = 'xx' or 1='1';
+------+------+------+
| id | name | pwd |
+------+------+------+
| 100 | zy | 123 |
+------+------+------+
1 row in set (0.00 sec)
解决方案1:使用预编译
//预编译后 万能密码失效
$conn = new mysqli('localhost','root','','test');
if ($conn->connect_errno){
die('连接失败'.$conn->connect_error);
}
$conn->query('set names utf8');
$sql = 'select * from users where id=? and pwd =?';
$stmt = $conn->prepare($sql) or die($conn->error);
$stmt->bind_param('is',$id,$pwd);
$stmt->bind_result($id,$name,$pwd);
$id = 100;
$pwd = "aaa' or 1='1";
//$pwd = '123';
if ($stmt->execute()){
while ($stmt->fetch()){
echo '<br>';
echo $id.'--'.$name.'--'.$pwd;
}
}else{
echo '查询失败'.$stmt->error;
}
$stmt->free_result();
$stmt->close();
$conn->close();
解决方案2:修改校验逻辑
旧方案:原本的校验逻辑,这种方式很容易被sql注入
$sql = "select * from users where id=100 and pwd = 'xxx'";
看看有没有查询到信息
if(查询到){
header('admin.php');
}else{
header('login.php');
}
新方案:修改校验逻辑
$sql = "select pwd from users where id=100";
看看有没有查询到信息
如果有 说明id存在
if(从数据库查询到的密码 == 用户输入的密码){
header('admin.php');
}else{
header('login.php');
}
解决方案2:加强
//解决方案2已经比较安全了,但仍有可能被攻击
在 DAOMySQLi 类中添加转义字符串方法,把字符串里面的 ' 等危险的东西处理掉,增加安全性
//转义字符串,并使用引号包裹
public function escapeString($str){
return "'".$this->_mysqli->real_escape_string($str)."'";
}