当前位置:首页>python>每天学习一点Python——SQL注入攻击与防御

每天学习一点Python——SQL注入攻击与防御

  • 2026-08-22 07:54:29
每天学习一点Python——SQL注入攻击与防御

每天学习一点Python——SQL注入攻击与防御

大家好,今天我们要聊一个非常实用但又容易被忽视的话题——SQL注入攻击。

相信很多初学者在写数据库操作代码时,都遇到过这样的情况:代码明明看起来没问题,但一执行就报错,而且错误信息特别奇怪。今天,我们就通过一个完整的例子,彻底搞明白这是怎么回事。

一、先来创建一个简单的数据库

让我们从最基础的开始。首先,我们需要创建一个数据库和一张表:

import sqlite3

sql = """
DROP TABLE IF EXISTS People;

CREATE TABLE People(
    FirstName TEXT,
    LastName TEXT,
    Age INT
);

INSERT INTO People VALUES(
    'Ron',
    'Obvious',
    42
);
"""


with
 sqlite3.connect("test_database.db") as connection:
    cursor = connection.cursor()
    cursor.executescript(sql)

代码逐行解释:

代码
作用
import sqlite3
导入Python内置的SQLite数据库模块
sql = """..."""
定义一个多行字符串,存放我们要执行的SQL语句
DROP TABLE IF EXISTS People;
如果已存在People表,就先删除它(避免冲突)
CREATE TABLE People(...);
创建新表,包含三个字段:FirstName(文本)、LastName(文本)、Age(整数)
INSERT INTO People VALUES(...);
插入一条测试数据:Ron Obvious,42岁
with sqlite3.connect("test_database.db") as connection:
连接数据库(如果文件不存在会自动创建)
cursor = connection.cursor()
创建游标对象,用于执行SQL语句
cursor.executescript(sql)
执行多条SQL语句(注意是executescript,不是execute)

执行这段代码后,我们就有了一个包含一条记录的数据库。

二、批量插入数据

接下来,我们用更高效的方式插入多条数据:

people_values = (("Ron", "Obvious", 42), ("Luigi", "Vercotti", 43), ("Arthur", "Belling", 28))
with
 sqlite3.connect("test_database.db") as connection:
    cursor = connection.cursor()
    cursor.executemany("INSERT INTO People VALUES(?, ?, ?)", people_values)

重点理解:三个问号是什么意思?

这里出现了三个?,很多初学者会困惑:为什么要用问号?直接写值不行吗?

这三个问号是参数占位符,它们对应的是:

  • • 第1个?:对应FirstName字段
  • • 第2个?:对应LastName字段
  • • 第3个?:对应Age字段

为什么正好是三个?看看我们的数据:

  • • ("Ron", "Obvious", 42) → 三个值
  • • ("Luigi", "Vercotti", 43) → 三个值
  • • ("Arthur", "Belling", 28) → 三个值

executemany的工作原理:

  1. 1. 准备SQL模板:INSERT INTO People VALUES(?, ?, ?)
  2. 2. 遍历people_values中的每个元组
  3. 3. 把每个元组的三个值依次替换到三个?的位置

相当于执行了三次插入:

INSERT INTO People VALUES('Ron', 'Obvious', 42)
INSERT INTO
 People VALUES('Luigi', 'Vercotti', 43)
INSERT INTO
 People VALUES('Arthur', 'Belling', 28)

三、验证数据是否正确插入

connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()

query = "SELECT * FROM People;"
results = cursor.execute(query)

results.fetchall()

执行结果:

[('Ron', 'Obvious', 42), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28)]

可以看到,三条数据都成功插入!

四、危险的字符串拼接方式

现在,假设我们要让用户输入信息并存入数据库。很多初学者会这样写:

first_name = input("Enter your first name:")
last_name = input("Enter your last name:")
age = int(input("Enter your age:"))

# 危险的字符串拼接

query = f"INSERT INTO People VALUES('{first_name}','{last_name}',{age});"
with
 sqlite3.connect("test_database.db") as connection:
    cursor = connection.cursor()
    cursor.execute(query)

这段代码有什么问题?

看起来挺合理的,对吧?让我们看看用户输入普通数据时的情况:

正常情况:用户输入 Flannery、OConnor(不带单引号)、42
生成的SQL:

INSERT INTO People VALUES('Flannery', 'OConnor', 42);

一切正常!

⚠️ 问题来了:当用户输入包含单引号

假设用户输入:

  • • FirstName: Flannery
  • • LastName: O'Connor(注意这个单引号!)
  • • Age: 42

拼接后的SQL:

INSERT INTO People VALUES('Flannery', 'O'Connor', 42);

数据库是怎么理解这段SQL的?

  1. 1. 第一个字符串:'Flannery' ✅ 正常
  2. 2. 第二个字符串:'O' ✅ 到这里还正常
  3. 3. 然后遇到 Connor', 42); — 这变成了裸的SQL代码,不是字符串了!

Python会很困惑:第一个引号开始,遇到第二个引号就结束了,后面的 Connor', 42);是什么鬼?

💡 小Tip:什么是SQL注入?

当用户输入的数据中包含特殊字符(如单引号),这些字符提前关闭了SQL语句中的字符串,导致后面的内容被当成SQL代码执行——这就是SQL注入的基本原理。黑客可以利用这一点执行恶意SQL命令!

五、安全的参数化查询方式

正确的写法应该是:

first_name = input("Enter your first name:")
last_name = input("Enter your last name:")
age = int(input("Enter your age:"))
data = (first_name, last_name, age)
with
 sqlite3.connect("test_database.db") as connection:
    cursor = connection.cursor()
    cursor.execute("INSERT INTO People VALUES(?, ?, ?)", data)

为什么这样写就安全了?

关键区别:数据和SQL代码被分开了!

对比项
字符串拼接方式
参数化查询方式
写法
f"INSERT...'{name}'...""INSERT...?, ?, ?", data
数据位置
混在SQL字符串里
作为单独的参数传递
单引号处理
被解析为SQL代码结束符
被自动转义为普通字符
安全性
❌ 危险
✅ 安全

参数化查询的工作原理:

  1. 1. 数据库先收到SQL模板:INSERT INTO People VALUES(?, ?, ?)
  2. 2. 数据库把这个模板编译好
  3. 3. 数据库再收到数据:("Flannery", "O'Connor", 42)
  4. 4. 数据库把数据原封不动地填进去,用户输入的任何特殊字符都会被自动转义

所以,当用户输入O'Connor时,数据库会自动把它转义成O''Connor(两个单引号在SQL中表示一个单引号),完美解决问题!

六、验证安全插入的结果

connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()

query = "SELECT * FROM People;"
results = cursor.execute(query)

results.fetchall()

执行结果:

[('Ron', 'Obvious', 42), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28), ('Flannery', 'O'Connor', 42)]

看,O'Connor被正确存储了!

七、更新数据操作

除了插入,更新数据时也要使用参数化查询:

# 将 Ron Obvious 的年龄更新为45岁
with
 sqlite3.connect("test_database.db") as connection:
    cursor = connection.cursor() 
    cursor.execute("UPDATE People SET Age=? WHERE FirstName=? AND LastName=?;", (45, 'Ron', 'Obvious'))

验证更新结果:

connection = sqlite3.connect("test_database.db")
cursor = connection.cursor()
query = "SELECT * FROM People;"
results = cursor.execute(query)
results.fetchall()

执行结果:

[('Ron', 'Obvious', 45), ('Luigi', 'Vercotti', 43), ('Arthur', 'Belling', 28), ('Flannery', 'O'Connor', 42)]

Ron的年龄从42变成了45,更新成功!

八、综合练习:星际迷航船员管理系统

让我们通过一个完整的例子,巩固今天学到的知识:

import sqlite3

# 1. 创建Roster表

create_roster = """
CREATE TABLE Roster(
    Name TEXT,
    Species TEXT,
    Age INT
);
"""


# 准备要插入的数据

roster_data = (
    ("Benjamin Sisko", "Human", 40),
    ("Jadzia Dax", "Trill", 300),
    ("Kira Nerys", "Bajoran", 29)
)

with
 sqlite3.connect("starfleet.db") as connection:
    cursor = connection.cursor()

    # 1. 创建表(如果存在则先删除)

    cursor.execute("DROP TABLE IF EXISTS Roster")
    cursor.execute(create_roster)
    print
("✓ 步骤1:Roster表创建成功")

    # 2. 插入数据(使用executemany批量插入)

    cursor.executemany(
        "INSERT INTO Roster VALUES(?, ?, ?);"
, 
        roster_data
    )
    print
("✓ 步骤2:数据插入成功")

    # 查询并显示所有数据

    cursor.execute("SELECT * FROM Roster;")
    for
 row in cursor.fetchall():
        print
(f"  姓名:{row[0]},种族:{row[1]},年龄:{row[2]}")

    # 3. 更新数据:将Jadzia Dax改为Ezri Dax

    cursor.execute(
        "UPDATE Roster SET Name=? WHERE Name=?;"
,
        ("Ezri Dax", "Jadzia Dax")
    )
    print
("✓ 步骤3:姓名更新成功")

    # 再次查询显示更新后的结果

    cursor.execute("SELECT * FROM Roster;")
    for
 row in cursor.fetchall():
        print
(f"  姓名:{row[0]},种族:{row[1]},年龄:{row[2]}")

    # 4. 查询所有Bajoran人

    cursor.execute(
        "SELECT Name, Age FROM Roster WHERE Species='Bajoran';"

    )

    print
("\n✓ 步骤4:Bajoran人查询结果:")
    for
 name, age in cursor.fetchall():
        print
(f"  姓名:{name},年龄:{age}")

执行结果:

✓ 步骤1:Roster表创建成功
✓ 步骤2:数据插入成功
  姓名:Benjamin Sisko,种族:Human,年龄:40
  姓名:Jadzia Dax,种族:Trill,年龄:300
  姓名:Kira Nerys,种族:Bajoran,年龄:29
✓ 步骤3:姓名更新成功
  姓名:Benjamin Sisko,种族:Human,年龄:40
  姓名:Ezri Dax,种族:Trill,年龄:300
  姓名:Kira Nerys,种族:Bajoran,年龄:29

✓ 步骤4:Bajoran人查询结果:
  姓名:Kira Nerys,年龄:29

💡 小Tip:fetchall()的返回值

注意代码中的for row in cursor.fetchall():,这里的row是一个元组,包含查询结果的一整行数据。我们可以通过索引访问每个字段:row[0]是第一个字段,row[1]是第二个字段...

而在最后的Bajoran查询中,我们直接用了for name, age in cursor.fetchall():,这是因为我们只查询了两个字段,可以直接解包赋值。

总结

今天我们学习了:

  1. 1. SQLite基础操作:创建表、插入数据、更新数据、查询数据
  2. 2. 参数占位符?:三个问号对应三个字段
  3. 3. executemany批量操作:高效插入多条数据
  4. 4. SQL注入的原理:用户输入的单引号提前关闭SQL字符串
  5. 5. 参数化查询的安全性:SQL代码和数据分开传递,自动转义特殊字符

记住一条黄金法则:永远不要用字符串拼接的方式构造SQL语句,始终使用参数化查询!

今天的Python学习就到这里,下次再见!


📦 资源获取提示

关注「码农自习室」,后台回复关键词 Python学习,即可获取本文完整代码及配套练习数据集,一起动手掌握高效数据操作的核心技巧!


❤️ 支持我们

如果觉得本文对你有帮助,欢迎点赞 + 推荐 + 关注,您的支持是我们持续创作优质内容的最大动力!


📚 学习资源说明

本文内容整理自《Python基础教程(第3版)》第15章,所有命令均已实测。

让我们一起坚持学习,每天进步一点点!💪

最新文章

随机文章