MySQL的SQL预编译及防SQL注入

x33g5p2x  于2021-10-21 转载在 Mysql  
字(6.1k)|赞(0)|评价(0)|浏览(399)

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_bind_param);
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;   //表示查询未被删除的记录

            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注入:

2.2 如何防止SQL注入:

# SQL注入与MySQL预编译:

IM项目中只有在 “insert into” 向表中插入数据时,才会使用 CPrepareStatement 预处理,
因为只有这个时候才会发生 “SQL注入”。

而MySQL预处理不仅 可以防止SQL注入,还有提高执行效率的作用:
《“即时SQL” 与 “预处理SQL” 的区别》:
https://www.cnblogs.com/geaozhang/p/9891338.html

参考链接:
https://dev.mysql.com/doc/c-api/5.6/en/c-api-prepared-statement-type-codes.html

相关文章