【python轻量级Web框架 Flask 】2 构建稳健 API:集成 MySQL 参数化查询与 DBUtils 连接池

前面我们了解了🎋 Flask框架的背景与特征、组成等,并通过过*++一个简单的Flask API实例++* 了解到框架的工作逻辑 --- --- "接收、匹配、处理、返回"

实例中的业务逻辑:任何人向我们的网站发送POST请求,都会给返回一个相应的sign签名

这类似于提供了一个公共服务,那如果我们只想给已授权用户提供服务,该怎么做呢?

1 基于文件授权

显然,我们需要做一个校验。我们将已授权用户信息存储在一个文件db.txt当中:

我们基于之前的案例进行修改:

在视图函数当中将文件数读取到内存,通过 user_dict存储信息,指明必须携带参数'token=......',之后验证token对应代码是否对应已授权用户,从而达到验证功能。

python 复制代码
import hashlib
import json

from flask import Flask, request, jsonify

# 创建按一个Flask实例对象
app = Flask(__name__)

# 获取用户信息-字典
def get_user_dict():
    info_dict = {}
    with open('db.txt', mode='r', encoding='utf-8') as f:
        for line in f:
            line = line.strip()
            token, name = line.split(',')
            info_dict[token] = name
    return info_dict

@app.route("/bili", methods=["POST"])
def bili():
    '''
    请求的URL需要携带: /bili?token=123456789
    请求的数据格式要求: {"ordered_string": "......"}
    :return:
    :return:
    '''
    # 是否携带参数
    token = request.args.get('token')
    if not token:
        return jsonify({"status": False, "error": "认证失败"})
    # 是否对应授权
    user_ditc = get_user_dict()
    print(user_ditc)
    if token not in user_ditc:
        return jsonify({"status": False, "error": "认证失败"})
    # 核心算法处理
    ordered_string = request.json.get('ordered_string')
    if not ordered_string:
        return jsonify({"status":False, "error":"参数错误1"})

    # 调用算法,处理接收到的数据
    encrypt_string = ordered_string + "123456789"
    obj = hashlib.md5(encrypt_string.encode('utf-8'))
    sign = obj.hexdigest()
    return jsonify({"status": True, 'data':sign})

if __name__ == '__main__':
    app.run()

// 运行效果:

case1:没有携带token参数:

case2:携带正确token参数

这里是一个简单校验,也可以考虑绑定IP、手机验证码登录、人脸识别等更为复杂安全的校验。


2 基于数据库授权

相比于.txt文件,数据库操作往往更加方便、简洁、规范。我们将用户信息放在MySQL数据库中,让Flask程序去连接MySQL数据进行授权和校验。

管理员直接用图形化界面连接MySQL开账户和授权。

(主包使用的是数据库图形化工具是Navicat*,一套功能强大的**数据库管理和开发工具****)*

2.1 导入 pymysql 包

📫 PyMySQL 是一个纯 Python 编写的 MySQL 客户端库。它的作用是让 Python 程序能够遵循 MySQL 的协议,与数据库服务器进行通信。

python 复制代码
pip install pymysql

PyMySQL 的核心工作流程

**建立连接:**通过pymsql.connect连接本地数据库(127.0.0.1),指定配置信息(用户名、密码等)

执行查询:

  • 游标(Cursor):用于执行SQL并获取结果的"指针";
  • 参数化查询:cursor.execute(sql,[params, ])将参数传入-注意:这里使用了一列表包装[params, ],这是为了防止SQL注入,是一种安全的写法。
  • 获取结果:fetchone-只从查询结果取出第一行数据,如果没有匹配项,返回None

资源释放与返回

2.2 代码实现:访问API,连接MySQL数据库进行凭证的校验

python 复制代码
import hashlib
import json

from flask import Flask, request, jsonify
import pymysql
# 创建按一个Flask实例对象
app = Flask(__name__)

#连接数据库读取数据
def fetch_one(sql, params):
    conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='123456', charset='utf8',
                           db='flask-learning')
    cursor = conn.cursor()
    # cursor.execute(sql, [params, ])
    # result = cursor.fetchone()
    # cursor.close()
    # conn.close()
    # return result
    
    '''如果 execute 报错,conn.close() 可能不会被执行。建议使用上下文管理器,即使出错也能自动关闭连接。离开 with 块后游标会自动关闭'''    
    with conn.cursor() as cursor:
        cursor.execute(sql, [params])
        result = cursor.fetchone()

@app.route("/bili", methods=["POST"])
def bili():
    '''
    请求的URL需要携带: /bili?token=123456789
    请求的数据格式要求: {"ordered_string": "......"}
    :return:
    :return:
    '''
    # 1.token是否为空
    token = request.args.get('token')
    if not token:
        return jsonify({"status": False, "error": "认证失败"})
    # 2.token是否合法,连接MySQL执行命令
    result = fetch_one('select * from user where token=%s', [token, ])
    if not result:
        return jsonify({"status": False, "error": "认证失败"})
    print(result)

    # 核心算法处理
    ordered_string = request.json.get('ordered_string')
    if not ordered_string:
        return jsonify({"status":False, "error":"参数错误1"})

    # 调用算法,处理接收到的数据
    encrypt_string = ordered_string + "123456789"
    obj = hashlib.md5(encrypt_string.encode('utf-8'))
    sign = obj.hexdigest()
    return jsonify({"status": True, 'data':sign})

if __name__ == '__main__':
    app.run()

// 运行效果:

讨论:SQL注入问题

SQL注入(SQL Injection)

这是一个非常经典且重要的问题,下面从"黑客视角"和"数据库视角"理解:

(1)正常场景

数据库执行这条指令,老老实实地返回了 ID 为 1 的用户信息。一切看起来很正常。

python 复制代码
user_id = "1"  # 假设这是用户从前端输入框传来的
sql = "SELECT * FROM users WHERE id = " + user_id
# 最终生成的 SQL:SELECT * FROM users WHERE id = 1

(2)攻击场景:SQL 注入

如果用户不是老老实实输入数字,而是在输入框里输入了:1 OR 1=1

python 复制代码
user_id = "1 OR 1=1"
sql = "SELECT * FROM users WHERE id = " + user_id
# 最终生成的 SQL:SELECT * FROM users WHERE id = 1 OR 1=1

🔨🔨🔨 这很危险!

  • 数据库在执行 WHERE id = 1 OR 1=1 时,会对表中的每一行进行判断。

  • 虽然某一行的 id 可能不是 1,但因为后面跟着 OR 1=1,整个判断条件依然成立

  • 结果 :数据库会毫无保留地把 users 表里的所有用户信息全部返回

(3)更严重的后果

如果用户输入的不是 OR 1=1,而是更狠的指令呢?比如输入:1; DROP TABLE users;

python 复制代码
SELECT * FROM users WHERE id = 1; DROP TABLE users;

第一条指令查出了数据,第二条指令直接把我们的用户表给删了。

方案: 参数化查询

使用%s作为占位符,将变量放在[]列表中传入:PyMySQL会自动处理转义字符,即使params包含恶意代码,也会被当作普通字符串处理,而不会当作SQL指令执行。


3 集成MySQL 数据库连接池

3.1 问题讨论

用户每次发送请求,后台与数据库会经历: ++建立连接-请求查询-返回-断开连接++ 这样一个过程。

------ 😥 这就好比你每去一次超市都要现买一辆新车,回来后再把车报废掉,效率低得离谱。

然而在实际开发当中,++相比于数据的发送与接收,连接的建立与关闭更为耗时++,这样效率很低。


😶 **如果我们让所有请求共用一个连接?****绝对不行!**可能存在并发的问题。
a) 并发冲突 :数据库游标(Cursor)不是线程安全的。如果 A 请求正在执行查询,B 请求突然进来了,它们会争抢同一个通道,导致报错(如 Commands out of sync)。

b) 事务错乱:若A请求开启了事务没提交,B请求紧接着执行操作,B 的操作可能会被错误地回滚或提交。

c) 连接失效:如果数据库因为超时主动断开了这个长连接,你的整个后端就挂了。


3.2 解决方案:数据库连接池(Connection Pooling)

逻辑:预先租好一排车(连接),谁用谁开,用完还回来。

核心原理:

a)初始化:程序启动时,先创建 N 个连接放在池子里。

b)借出 (Acquire):当请求进来时,从池子里取出一个"空闲"连接。

c)使用:执行 SQL 操作。

d)归还 (Release) :操作完成后,不关闭连接,而是放回池子,标记"空闲", 等待下一个请求。

在 Python/Flask 环境中,最常用的连接池工具是 DBUtils

python 复制代码
pip install DBUtils

编写连接池工具类:

(下面是修改部分,其余无改动)

python 复制代码
import pymysql
from dbutils.pooled_db import PooledDB

# 1. 初始化连接池配置
POOL = PooledDB(
    creator = pymysql,       # 使用链接数据库的模块
    maxconnections = 10,      # 连接池允许的最大连接数,0和None表示不限制连接数
    mincached = 2,           # 初始化时,连接池中至少1创建的空闲的链接,0表示不创建
    maxcached = 5,           # 连接池中最多闲置的连接数,0和None表示不限制
    blocking=True,           # 连接池中如果没有可用连接后,是否阻塞等待。True:等待;Flase:不等待,报错
    setsession= [],          # 开始会话前执行的命令列表。如:["set datastyle to ...", "set time zone ..."]
    ping = 0,
    # 数据库配置信息指定
    host='127.0.0.1', port=3306, user='root', password='123456', charset='utf8',db='flask-learning'
)

# 2. 连接数据库读取数据
def fetch_one(sql, params):
    # conn = pymysql.connect(host='127.0.0.1', port=3306, user='root', password='123456', charset='utf8',db='flask-learning')
    # 从池子里拿一个连接,而不是新建
    conn = POOL.connection()
    cursor = conn.cursor(pymysql.cursors.DictCursor)  # 推荐用字典游标
    try:
        cursor.execute(sql, params)
        result = cursor.fetchone()
    finally:
        # 这里的 close() 不会真的断开数据库,而是把连接还回池子
        cursor.close()
        conn.close()
    return result

总结

在前面的练习中,我们针对 "生成签名" 案例进行了Flask API访问的简单实现,从开始的无校验版本,到基于文件授权来限制用户访问,为了更加规范并且数据操作简洁,我们实现基于数据库的授权------ 用户发送请求-后台与数据库:建立连接-请求查询-返回结果-断开连接。

然而,频繁的连接的建立与断开会极大降低程序的性能,在实际开发当中,可以通过集成 DBUtils 连接池来避免这个问题:

a) 省去了握手和认证时间,让响应速度大幅提升;b) 同时通过 maxconnections 限制,防止后端把数据库连接数撑爆;c) 资源复用,降低了 CPU 和内存的波动。

相关推荐
Alaaaaaaan2 小时前
[DevOps]使用github-action工具部署docker容器(实现提交代码一键推送部署到服务器)
服务器·前端·docker·容器·github
面对疾风叭!哈撒给2 小时前
Windows 系统安装 Mysql 8.0+
数据库·windows·mysql
2301_810730102 小时前
python第四次作业
数据结构·python·算法
马剑威(威哥爱编程)2 小时前
Libvio.link爬虫技术解析:搞定反爬机制
爬虫·python
zhougl9962 小时前
Java 枚举类(enum)详解
java·开发语言·python
恋爱绝缘体12 小时前
Java语言提供了八种基本类型。六种数字类型【函数基数噶】
java·python·算法
摇滚侠2 小时前
css 设置边框
前端·css
serve the people2 小时前
python环境搭建 (三) FastAPI 与 Flask 对比
python·flask·fastapi
·云扬·3 小时前
MySQL Binlog三种记录格式详解
android·数据库·mysql