MySQL的SQL预编译及防SQL注入

文章目录

  • 1 SQL语句的执行处理
    • 1.1 即时SQL
    • 1.2 预处理SQL
      • 1.2.1 预编译SQL的实现步骤
      • 1.2.2 预编译SQL的C++使用举例
      • 1.2.3 MYSQL_BIND()函数中的参数类型:
  • 2 SQL注入
    • 2.1 什么是SQL注入
    • 2.2 如何防止SQL注入

1 SQL语句的执行处理

SQL的执行可大致分为下面两种模式:

“Immediate Statements” VS “Prepared Staements” :

1.1 即时SQL

动态的根据传入的参数拼接SQL语句并执行,一条语句经过MySQL server层分析器、优化器、执行器组件,分别进行词法、语义解析、优化SQL语句、选择索引、制定执行计划、执行并返回结果。

对SQL语句进行词法语义分析、优化SQL语句、选择索引、制定执行计划等一系列操作,称为 “对SQL语句的编译”。

如上,一条SQL语句按照此流程处理,一次编译,单次运行,此类普通语句被称作 “Immediate Statements”(即时SQL)

例如:

bool CUserModel::getUser(uint32_t nUserId, DBUserInfo_t &cUser) 
{CDBConn* pDBConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave");if(pDBConn) {//根据函数外部传入的参数 nUserId,动态构造 select查询语句并执行:string strSql = "select * from IMUser where id = " + int2string(nUserId);CResultSet* pResultSet = pDBConn->ExcuteQuery(strSql.c_str());if(pResultSet) {while(pResultSet->Next()) {//...}}} 
}

但是,绝大多数情况下,一般会需要一条SQL语句反复调用执行(例如上面的查找IMUser表中的用户信息,每次客户端向服务器请求登录验证时都需要执行一次),或者每次执行的时候只有个别的值不同(比如select的where子句值不同,update的set子句值不同,insert的values子句值不同)。

如果每次都需要经过上面的SQL编译过程(词法语义分析、语句优化、制定执行计划等),则效率明细会受到影响。

1.2 预处理SQL

所谓 “预编译SQL语句”,就是将此类SQL语句中的某些值使用 “占位符” 替代,可以视为将SQL语句 “模板化” 或者说 “参数化”。一般称这类语句为 “Prepared Statements”

预编译SQL语句的优势在于:一次编译、多次运行,省去了解析、优化等过程。此外使用预编译SQL语句还能防止SQL注入,下文展开。

1.2.1 预编译SQL的实现步骤

(1)先与MySQL数据库取得连接,获得 “连接句柄” MYSQL*

MYSQl* mysql_init();
mysql_options();
mysql_real_connect(MYSQL*, ip, user_name, passed, db_name, port);

(2)基于这个 MYSQL* 连接句柄,初始化一个“预编译句柄”MYSQL_STMT*

MYSQL_STMT* mysql_stmt_init(MYSQL*);

(3)传入准备好的带有“占位符”的SQL语句,进行编译:

mysql_stmt_prepare(MYSQL_STMT*, sql.c_str(), sizeof(sql));

(4)在后面要使用这个预编译的SQL语句时,需要向其中传入实参填补“占位符”,所以我们必须要先将占位符的个数统计出来,并预先初始化一个 MYSQL_BIND类型的结构体数组(MYSQL_BIND[]数组的元素个数是SQL语句中占位符的个数,数组中每个元素是MYSQL_BIND结构体,用于指定某个占位符上的数据类型(如int) 及 数据值),等待使用时向其中填充参数:

uint32_t m_param_cnt = mysql_stmt_param_count(MYSQL_STMT*);
MYSQL_BIND* m_param_bind = new MYSQL_BIND[m_param_cnt];	//新建一个数组

(5)在使用时,先给 MYSQL_BIND[] 数组填充值:

for(int index = 0; index < m_param_cnt; index++) 
{//如果value是int型:MYSQL_BIND[index].buffer_type = MYSQL_TYPE_LONG; MYSQL_BIND[index].buffer = &value;/*//如果value是string型:MYSQL_BIND[index].buffer_type = MYSQL_TYPE_LONG; MYSQL_BIND[index].buffer = (char*)value.c_str();MYSQL_BIND[index].buffer_length = value.size();*/
}

(6)向填充好实参的MYSQL_BIND数组传入MYSQL_STMT句柄,随后执行这条SQL语句,并检查执行结果:

msyql_stmt_bind_param(m_stmt, m_param_bind );
mysql_stmt_excute(m_stmt);		//如果有错误发生,函数返回非0,使用 mysql_stmt_error(m_stmt);可检查错误原因
mysql_stmt_affected_rows(m_stmt) == 0;

1.2.2 预编译SQL的C++使用举例

实现一个 CPrepareStatement 类,封装 MYSQL_STMT* 和 MYSQL_BIND* 对象,即相应的SQL预编译方法:


//cpreparestatement.hclass CPrepareStatement {
public:CPrepareStatement() {}~CPrepareStatement() {}bool Init(MYSQL* mysql, string& sql);void SetParam(uint32_t index, int& value);void SetParam(uint32_t index, uint32_t& value);void SetParam(uint32_t index, string& value);void SetParam(uint32_t index, const string& value);bool ExecuteUpdate();uint32_t GetInsertId();private:MYSQL_STMT*   m_stmt;MYSQL_BNID*   m_param_bind;uint32_t      m_param_cnt;
};//cpreparement.cppbool CPrepareStatement::Init(MYSQL* mysql, string& sql) {mysql_ping(mysql);m_stmt = mysql_stmt_init(mysql);if(!m_stmt) {return false;}if(mysql_stmt_prepare(m_stmt, sql.c_str(), sql.size())) {printf("%s\n", mysql_stmt_error(m_stmt));return false;}m_param_cnt = mysql_stmt_papram_count(m_stmt);if(m_param_cnt > 0) {m_param_bind = new MYSQL_BIND[m_param_cnt];if(!m_param_bind) {return false;}}memset(m_param_bind, 0, sizeof(MYSQL_BIND) * m_param_cnt);return true;
}//注意:给int型和string型赋值的方式是不同的:
void CPrepareStatement::SetParam(uint32_t index, int& value) {if(index >= m_param_cnt)return;m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;m_param_bind[index].buffer = &value;
}void CPrepareStatement::SetParam(uint32_t index, uint32_t& value) {if(index >= m_param_cnt)return;m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;m_param_bind[index].buffer = &value;
}void CPrepareStatement::SetParam(uint32_t index, string& value) {if(index >= m_param_cnt)return;m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;m_param_bind[index].buffer = (char*)value.c_str();m_param_bind[index].buffer_length = value.size();
}void CPrepareStatement::SetParam(uint32_t index, const string& value) {if(index >= m_param_cnt)return;m_param_bind[index].buffer_type = MYSQL_TYPE_LONG;m_param_bind[index].buffer = (char*)value.c_str();m_param_bind[index].buffer_length = value.size();
}bool CPrepareStatement::ExecuteUpdate() {if(!m_stmt)return false;if(mysql_stmt_bind_param(m_stmt, m_param_bind)) {printf("%s\n", mysql_stmt_error(m_stmt));return false;}if(mysql_stmt_execute(m_stmt)) {printf("%s\n", mysql_stmt_error(m_stmt));return false;}if(msyql_affected_rows(m_stmt) == 0) {printf("no affect\n");return false; }return true;
}uint32_t CPrepareStatement::GetInsertId() {return mysql_stmt_insert_id(m_stmt);
}

使用 class CPrepareStatement 类执行insert into插入操作:

bool CMessageModel::sendMessage(uint32_t nRelateId, uint32_t nFromId, uint32_t nToId, IM::BaseDefine::MsgType nMsgType, uint32_t nCreateTime, uint32_t nMsgId, string& strMsgContent) {CDBConn* pDBConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave");if(pDBConn) {string strTableName = "IMMessage_" + int2string(nRelateId % 8);string strSql = "insert into " + strTableName + " ('relateId', 'fromId', 'toId', 'msgId', 'content', 'status', 'type', 'created', 'updated') values (?, ?, ?, ?, ?, ?, ?, ?, ?)";shared_ptr<CPrepareStatement> pStmt = make_shared<CPrepareStatement>();if(pStmt->Init(pDBConn->GetMysql(), strSql)) {uint32_t nStatus = 0;   //表示查询未被删除的记录uint32_t index = 0;pStmt->SetParam(index++, nRelateId);pStmt->SetParam(index++, nFromId);pStmt->SetParam(index++, nToId);pStmt->SetParam(index++, nMsgId);pStmt->SetParam(index++, strMsgContent);pStmt->SetParam(index++, nStatus);pStmt->SetParam(index++, nMsgType);pStmt->SetParam(index++, nCreateTime);pStmt->SetParam(index++, nCreateTime);pStmt->ExecuteUpdate();}//delete pStmt; 使用shared_ptr智能指针,不必delete删除pDBManager->RelDBConn(pDBConn); //这里同样可以使用RAII的方法实现自动释放,在 CDBConn类对象析构的时候释放连接}
}

1.2.3 MYSQL_BIND()函数中的参数类型:

MYSQL_BIND() 函数中的参数类型如下表所示,可见 MYSQL_TYPE_LONG 表示的是 4字节的int型。
在这里插入图片描述

2 SQL注入

2.1 什么是SQL注入

所谓SQL注入,就是通过把SQL命令插入到Web表单提交或页面请求url的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。具体来说,它是利用现有应用程序,将(恶意)的SQL命令注入到后台数据库引擎执行的能力,它可以通过在Web表单中输入(恶意)SQL语句得到一个存在安全漏洞的网站上的数据库,而不是按照设计者意图去执行SQL语句。

实战举例

有个登陆框如下:
在这里插入图片描述
可以看到除了账号密码之外,还有一个公司名的输入框,根据输入框的形式不难推出SQL的写法如下:

SELECT * From table_name WHERE name=‘XX’ and password=‘YY’ and corporate=‘ZZ’

怎么做呢?
在这里插入图片描述
因为没有校验,因此,我们账号密码,都不填写,直接在最后,添加 or 1=1 –

看看与上面SQL组合,成了如下:

SELECT * From table_name WHERE name=’’ and password=’’ and corporate=’’ or 1=1-’

从代码可以看出,前一半单引号被闭合,后一半单引号被 “–”给注释掉,中间多了一个永远成立的条件“1=1”,这就造成任何字符都能成功登录的结果。

重要提醒
不要以为在输入框做个检查就够了,不要忘记了,我们web提交表单,是可以模拟url直接访问过去,绕开前段检查。因此,必须是后端,或是数据来检查才能有效防止。

(1)检查用户输入的合法性;

(2)将用户的登录名、密码等数据加密保存。

(3)预处理SQL。

(4)使用存储过程实现查询,虽然不推荐,但也是一个方法。

2.2 如何防止SQL注入

其实是因为SQL语句在程序运行前已经进行了预编译,在程序运行时第一次操作数据库之前,SQL语句已经被数据库分析,编译和优化,对应的执行计划也会缓存下来并允许数据库已参数化的形式进行查询,当运行时动态地把参数传给PreprareStatement时,即使参数里有敏感字符如 or '1=1’也数据库会作为一个参数一个字段的属性值来处理而不会作为一个SQL指令,如此,就起到了SQL注入的作用了!

具体像这样。例如刚刚那条SQL:

SELECT * From table_name WHERE name=’’ and password=’’ and corporate=’’ or 1=1-’

开启预编译执行SQL的时候,则不会这么处理。会当成一个属性值。什么意思。随便你怎么加,都是一个值。也就是说,如果中间有产生歧义的,都将被处理掉,最后执行相当于是这样:

SELECT * From table_name WHERE name=’’ and password=’’ and corporate="'or 1=1–"

输入的一串,都被揉在一起,作一个参数,而不是SQL指令。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/176579.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《视觉SLAM十四讲》-- 后端 1(下)

8.2 BA 与图优化 Bundle Adjustment 是指从视觉图像中提炼出最优的 3D 模型和相机参数&#xff08;内参和外参&#xff09;。 8.2.1 相机模型和 BA 代价函数 我们从一个世界坐标系中的点 p \boldsymbol{p} p 出发&#xff0c;把相机的内外参数和畸变都考虑进来&#xff0c;…

黄执中老师人际说服课思考总结(个人笔记整理 ②)

前言&#xff1a; 沟通和说服的区别&#xff1a;为什么沟通不能解决问题&#xff0c;处于劣势的一方&#xff08;承受问题的那方&#xff09;才想去沟通&#xff08;对方没有沟通动力&#xff09;。说服是温柔而有力的学科 - 劣势一方的武器。 说服是一门影响人的学问&#xff…

golang工程组件——redigo使用(redis协议,基本命令,管道,事务,发布订阅,stream)

redisgo redis 与 client 之间采用请求回应模式&#xff0c;一个请求包对应一个回应包&#xff1b;但是也有例外&#xff0c;pub/sub 模 式下&#xff0c;client 发送 subscribe 命令并收到回应包后&#xff0c;之后被动接收 redis 的发布包&#xff1b;所以若需要使 用 pub/s…

vue 使用js new Map()优化多个if else 执行方法

前言 在实际开发中根据业务需求我们经常要判断情况&#xff0c;一个if 我们科技直接使用ES6就可以解决 经常会出现根据不同的条件执行不同的方法&#xff0c;这是就会有多个if else 看起不太美观也费劲 js new map &#xff08;&#xff09;就可以解决这个问题&#xff0c;它…

Java封装一个根据指定的字段来获取子集的工具类

工具类 ZhLambdaUtils SuppressWarnings("all") public class ZhLambdaUtils {/*** METHOD_NAME*/private static final String METHOD_NAME "writeReplace";/*** 获取到lambda参数的方法名称** param <T> parameter* param function functi…

全志R128基础组件开发指南——图像采集

图像采集 CSI&#xff08;DVP&#xff09; 图像采集 SENSOR -> CSI 通路 CSI &#xff08;CMOS sensor interface&#xff09;接口时序上可支持独立 SYNC 和嵌入 SYNC(CCIR656)。支持接收 YUV422 或 YUV420 数据。 VSYNC 和HSYNC 的有效电平可以是正极性&#xff0c;也可…

流量分析(5.5信息安全铁人三项赛数据赛题解)

黑客通过外部的web服务器攻击到企业内部的系统中&#xff0c;并留下了web后门&#xff0c;通过外部服务器对内部进行了攻击。 目录 黑客攻击的第一个受害主机的网卡IP地址 黑客对URL的哪一个参数实施了SQL注入 第一个受害主机网站数据库的表前缀(加上下划线 例如abc_) 第一…

当路由器突然提示“未检测到入户网线”

找到浏览器的“安全DNS”&#xff0c;根据浏览器不同可能有差别。 开启安全DNS&#xff0c;使用“自定义” 地址用阿里的&#xff08;其它的也行&#xff09;&#xff1a; https://dns.alidns.com/dns-query{?dns}

【工程部署】在RK3588上部署OCR(文字检测识别)(DBNet+CRNN)

硬件平台&#xff1a; 1、firefly安装Ubuntu系统的RK3588&#xff1b; 2、安装Windows系统的电脑一台&#xff0c;其上安装Ubuntu18.04系统虚拟机。 参考手册&#xff1a;《00-Rockchip_RKNPU_User_Guide_RKNN_API_V1.3.0_CN》 《RKNN Toolkit Lite2 用户使用指南》 1、文…

AI创作系统ChatGPT网站源码+支持最新GPT-Turbo模型+支持DALL-E3文生图/AI绘画源码

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如…

【uni-app + uView】CountryCodePicker 国家区号组件

1. 效果图 2. 组件完整代码 <template><u-popup class="country-code-picker-container" v-if="show" :show

基于Qt 多线程(继承 QObject 的线程)

​ 继承 QThread 类是创建线程的一种方法,另一种就是继承QObject 类。继承 QObject 类更加灵活。它通过 QObject::moveToThread()方法,将一个 QObeject的类转移到一个线程里执行。恩,不理解的话,我们下面也画个图捋一下。 通过上面的图不难理解,首先我们写一个类继承 QObj…