【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 和内存的波动。

相关推荐
love530love1 天前
Scoop 完整迁移指南:从 C 盘到 D 盘的无缝切换
java·服务器·前端·人工智能·windows·scoop
哈里谢顿1 天前
Django 应用 OOM(Out of Memory)故障的定位思路和排查方法
python·django
甄心爱学习1 天前
【python】获取所有长度为 k 的二进制字符串
python·算法
王码码20351 天前
Flutter for OpenHarmony:Flutter 三方库 bluez 玩转 Linux 风格的蓝牙操作(蓝牙底层互操作)
linux·运维·服务器·前端·flutter·云原生·harmonyos
tuotali20261 天前
氢气压缩机技术规范亲测案例分享
人工智能·python
嫂子的姐夫1 天前
030-扣代码:湖北图书馆登录
爬虫·python·逆向
a1117761 天前
EasyVtuber(或其衍生/增强版本)的虚拟主播(Vtuber)面部动画生成与直播解决方案
python·虚拟主播
lintax1 天前
计算pi值-积分法
python·算法·计算π·积分法
小凯123451 天前
pytest框架-详解(学习pytest框架这一篇就够了)
python·学习·pytest
逻极1 天前
pytest 入门指南:Python 测试框架从零到一(2025 实战版)
开发语言·python·pytest