MySQL的SQL预编译及防SQL注入

news2024/12/30 3:01:33

文章目录

  • 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.h

class 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.cpp

bool 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.coloradmin.cn/o/1205412.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux环境实现mysql所在服务器定时同步数据文件到备份服务器(异地容灾备份场景)

目录 概述 1、建立ssh连接 1.1、操作mysql所在服务器 1.2、操作备份文件服务器 2、创建脚本实现备份以及传输 3、配置定时任务 概述 应对异地容灾备份场景&#xff0c;mysql所在服务器和本分服务器需要建立ssh连接&#xff0c;每天mysql服务器通过定时任务执行脚本&…

微信小程序授权登陆 getUserProfile

目录 前言 步骤&#xff1a; 示例代码: 获取用户信息的接口变化历史: 注意事项&#xff1a; 前言 在微信小程序中&#xff0c;你可以使用 getUserProfile 接口来获取用户的个人信息&#xff0c;并进行授权登录。以下是使用 getUserProfile 的步骤&#xff1a; 小程序发了…

ChatGPT 4 OpenAI 数据分析动态可视化案例

数据分析可视化是一种将原始数据转化为图形或图像的方法,使得数据更易理解和解读。这种方法能够帮助我们更清楚地看到数据中的模式、趋势和关联性,从而更好地理解数据,并据此做出决策。 数据分析可视化的一些常见形式包括: 1. 折线图:常用于展示数据随时间的变化趋势。 …

MATLAB | 官方举办的动图绘制大赛 | 第一周赛情回顾

嘿真的又是很久没见了&#xff0c;最近确实有点非常很特别小忙&#xff0c;今天带来一下MATHWORKS官方举办的迷你黑客大赛第三期(MATLAB Flipbook Mini Hack)的最新进展&#xff01;&#xff01;目前比赛已经刚好进行了一周&#xff0c;前两届都要求提交280个字符内的代码来生成…

一个关于jdbc操作mysql和java基础练手的通讯录管理系统小项目

首先 : 整个项目的项目结构为 : 1.第一步先导入数据库的驱动&#xff0c;我的mysql数据库是8.0以上版本&#xff0c;然后导入的驱动就是8.0.16版本的jar包&#xff1b; 1.JdbcBase : JDBC基础操作封装成了JdbcBase类,在里面先静态定义了数据库连接对象和DQL查询结果&#x…

C++阶段复习‘‘‘‘总结?【4w字。。。】

文章目录 前言类和对象C类定义和对象定义类成员函数C 类访问修饰符公有&#xff08;public&#xff09;成员私有&#xff08;private&#xff09;成员受保护&#xff08;protected&#xff09;成员 继承中的特点类的构造函数和析构函数 友元函数内联函数this指针指向类的指针类…

蓝桥杯 冒泡排序

冒泡排序的思想 冒泡排序的思想是每次将最大的一下一下移动到最右边&#xff0c;然后将最右边这个确定下来。 再来确定第二大的&#xff0c;再确定第三大的… 对于数组a[n]&#xff0c;具体来说&#xff0c;每次确定操作就是从左往右扫描&#xff0c;如果a[i]>a[i1],我们将…

Prim算法(C++)

目录 介绍&#xff1a; 代码&#xff1a; 结果&#xff1a; 介绍&#xff1a; Prim算法是一种用于解决最小生成树问题的贪心算法。该算法的主要思想是从一个顶点开始&#xff0c;不断向图中添加边&#xff0c;直到构成一棵包含所有顶点的生成树&#xff0c;使得树的边权之…

VSCode 好用的插件分享

文章目录 Introlistcode runner 【在文本编辑器中编辑好各类语言的源代码&#xff0c;然后一键运行】gitlens - 【git提交信息即时查看&#xff0c;类似IDEA中的 show annotation】还有更多&#xff0c;会日常补充。 Intro 大四毕业前&#xff0c;我只有一台dell latitude 455…

Vue 小黑记事本组件板

渲染功能&#xff1a; 1.提供数据&#xff1a; 提供在公共的父组件 App.vue 2.通过父传子&#xff0c;将数据传递给TodoMain 3.利用 v-for渲染 添加功能&#xff1a; 1.收集表单数据 v-model 2.监听事件&#xff08;回车点击都要添加&#xff09; 3.子传父&#xff0c;讲…

Vue3 + Three.js + gltf-pipeline大型园区场景渲染与3D业务

在非使用unity作为3D渲染方案的前提下&#xff0c;对与目前web开发者比较友好的除了canvas场景需要的2D babylon.js&#xff0c;fabric.js, Three.js是目前针对于jsWeb用户最直接且比较友好的3D引擎方案了。 准备工作&#xff1a; 1.明确需要用的场景方案都有那些&#xff0c;模…

创建maven的 java web项目

创建maven的 java web项目 创建出来的项目样子 再添加java和resources文件夹 一定要如图有文件夹下有图标才代表被IDEA识别&#xff0c;不让是不行的 没有的话在File——ProjectStructure中进行设置

EasyDarwin开源流媒体服务器

文章目录 前言一、EasyDarwin 简介二、EasyDarwin 主要功能特点三、安装部署四、推拉流测试1、进入控制页面2、推流测试3、拉流测试 前言 本文介绍一个十分实用的高性能开源 RTSP 流媒体服务器&#xff1a;EasyDarwin。 一、EasyDarwin 简介 EasyDarwin 是基于 go 语言研发&a…

域名反查Api接口——让您轻松查询域名相关信息

在互联网发展的今天&#xff0c;域名作为网站的唯一标识符&#xff0c;已经成为了企业和个人网络营销中不可或缺的一部分。为了方便用户查询所需的域名信息&#xff0c;API接口应运而生。本文将介绍如何使用挖数据平台《域名反查Api接口——让您轻松查询域名相关信息》进行域名…

【Java笔试强训】Day10(CM62 井字棋、HJ87 密码强度等级)

CM62 井字棋 链接&#xff1a;井字棋 题目&#xff1a; 给定一个二维数组board&#xff0c;代表棋盘&#xff0c;其中元素为1的代表是当前玩家的棋子&#xff0c;0表示没有棋子&#xff0c;-1代表是对方玩家的棋子。当一方棋子在横竖斜方向上有连成排的及获胜&#xff08;及…

Ps:通过显示大小了解图像的打印尺寸

在 Photoshop 中&#xff0c;如果想了解文档窗口中的图像打印出来之后的实质大小&#xff0c;只要知道两个数值即可。 第一个数值是图像分辨率&#xff08;也称“文档分辨率”&#xff09;的大小&#xff0c;可在Ps菜单&#xff1a;图像/图像大小 Image Size对话框中查询或设置…

linux 安装 mini conda,linux下安装 Miniconda

下载地址 https://docs.conda.io/projects/miniconda/en/latest/index.html 安装conda mkdir -p ~/miniconda3 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda3/miniconda.sh bash ~/miniconda3/miniconda.sh -b -u -p ~/mini…

第1关:构造函数与析构函数的实现

题目&#xff1a;根据.h写出.cpp 考点&#xff1a; 1.链表的默认构造&#xff0c; 拷贝构造&#xff0c;传参构造以及析构函数等。 代码&#xff1a; /********** BEGIN **********/ #include <cstdlib> #include <cstring> #include "LinkedList.h&…

Java 设计模式——组合模式

目录 1.概述2.结构3.实现3.1.抽象构件3.2.容器构件3.3.叶子节点3.4.测试 4.分类5.使用场景6.优点 1.概述 &#xff08;1&#xff09;大家对于上面这个图片肯定非常熟悉&#xff0c;上图我们可以看做是一个文件系统&#xff0c;对于这样的结构我们称之为树形结构。在树形结构中可…

JDK并发修改异常的一个“BUG“

很多电商公司早期的架构都是基于PHP&#xff0c;所以我身边会有很多很厉害的PHP老哥&#xff0c;但现在都在写Java。昨天看到他在看Java的并发修改异常&#xff0c;正打算秀一波操作&#xff0c;却被他的一个问题难住了&#xff1a; public class ForeachTest {public static …