尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从SQL权限到安全编程:Python数据库实战Day40全记录

从SQL权限到安全编程:Python数据库实战Day40全记录 很多自学Python的朋友学到第40天左右正好是第一次有能力把Python和数据库串起来做点正经东西的阶段。语法看完了增删改查也会写了但有两个东西几乎所有人都下意识跳过去了SQL权限管理和安全编程。不是不想学是觉得自己只是本地练手、写个课程设计用不着搞那么复杂。我在带新人、帮人看代码的时候见过太多这种心态导致的真实事故有人用root账号跑业务代码误执行了一条DELETE连备份都没有整张表的数据直接清零有人把数据库密码硬编码在源码里代码传到公开仓库之后几分钟内就被扫描工具抓到数据库被人拖了库还有人一直用字符串拼接方式写SQL接到一个外部输入后整张用户表差点被清空。这篇就是Day40的完整实践记录把SQL权限管理和Python安全编程这两块一次性讲透并且给出一套可以直接抄作业的落地方案。内容基于我自己在项目中反复验证过的做法适合正在学习Python、准备把数据库能力用到真实项目里的读者。1. 为什么权限管理和安全编程要放在一起学先聊一个很多人没想明白的问题权限管理和安全编程看起来是两件事为什么要放在同一天讲因为在实际项目中它们本来就是一套组合拳。权限管理解决的是谁能碰数据库的问题安全编程解决的是碰数据库的时候会不会出问题的问题。如果只做权限管理而不做安全编程等于把门锁得很严但窗户大开攻击者根本不需要走门如果只做安全编程而不做权限管理等于窗户关好了但管理员钥匙乱发内部人员随手就能打开金库。两个环节单独看都有道理合在一起才是完整的防线。从攻击者的视角更好理解。一次典型的数据库攻击通常分两步第一步是找到进入系统的入口这一步靠SQL注入、弱口令猜解等方式完成第二步是提升权限或扩大破坏范围这一步靠的就是数据库账号权限过大。如果你在Python代码层面挡住了第一步攻击者根本走不到第二步如果你的数据库账号权限控制得当即便第一步失守损失也被限制在很小的范围内。所以只要做真实项目这两块就必须同时过关。Day40放在这个时间点还有一个现实原因学到这里绝大多数人的第一个完整Web项目已经上马了。项目一旦跑起来就会接触用户输入、接触数据库连接、接触部署环境这三个接触点恰好是权限管理和安全编程的主战场。现在不把习惯养好等项目代码量上去之后再回头改成本和风险都会翻很多倍。2. 权限设计的基本盘从最小权限原则到账号拆分2.1 业务代码为什么不能用root账号这是我最想强调的一点任何业务代码都不应该使用数据库的root账号连接数据库。这里的任何包括本地开发环境下的练手项目。原因是多层次的。第一层是爆炸半径问题。root账号相当于这个数据库实例的总经理它不仅能读写业务表还能改表结构、删表、删库、修改账号权限、查看所有数据库。一旦Python代码里有一个SQL注入漏洞攻击者用这个账号连上数据库之后不只是偷一份数据的问题而是可以把整个数据库服务拆了。用一个生活化的类比你不应该把家里的总钥匙交给送快递的人哪怕他看起来很老实万一被坏人抢了呢第二层是误操作的兜底问题。就算是自己人操作root权限也意味着没有缓冲。一个DELETE语句忘了加WHERE如果是业务账号可能权限只覆盖某几张表如果是root整个库的基业都保不住。我自己就见过不止一个开发者在测试环境用root账号执行了没带WHERE条件的UPDATE把整张表的某个字段刷成了同一个值最后只能靠备份恢复。第三层是审计和安全追溯问题。所有人都用root连数据库出了问题之后根本查不到是哪个人、哪个应用在什么时间做的操作。真实项目里一旦发生数据问题第一件事就是看数据库日志和账号使用记录如果都是root排查线索直接断掉。2.2 最小权限原则的落地按职责拆分账号最小权限原则Principle of Least Privilege是权限管理的核心指导思想翻译成大白话就是一个账号只拥有完成自己任务所必需的最小权限集合多一份都不给。在Python项目里我建议至少按下面这个模型拆分账号账号类型使用场景权限范围使用方只读账号报表查询、数据统计、后台看板SELECT数据分析脚本、后台只读页面读写账号业务日常CRUDSELECT / INSERT / UPDATE / DELETE主业务服务结构账号数据库表结构变更、迁移CREATE / ALTER / INDEX等DDL权限迁移工具、发布流程管理账号数据库运维管理对应库或实例的管理权限DBA或项目负责人实际项目里最常见的划分是把读写账号和结构账号分开。业务代码运行时只需要增删改查数据不需要改表结构所以它不应该拥有CREATE、ALTER这类DDL权限。这样即便业务代码被攻击者控制攻击者也无法通过这个连接修改你的表结构来植入恶意对象。2.3 一个可以直接用的权限拆分示例以MySQL 8.0为例假设我们的项目名叫blog业务账号和结构账号可以这样创建-- 创建业务数据库 CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8mb4; -- 创建业务读写账号应用运行时使用 CREATE USER blog_applocalhost IDENTIFIED BY 这里是强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON blog.* TO blog_applocalhost; -- 创建结构变更账号迁移工具使用 CREATE USER blog_ddllocalhost IDENTIFIED BY 这里是另一个强密码; GRANT CREATE, ALTER, DROP, INDEX, REFERENCES ON blog.* TO blog_ddllocalhost; -- 创建只读账号运营报表使用 CREATE USER blog_readonlylocalhost IDENTIFIED BY 这里是第三个强密码; GRANT SELECT ON blog.* TO blog_readonlylocalhost;注意几个细节主机范围尽量写具体。本地开发可以写localhost服务器环境写应用所在的主机IP。写%表示任意主机可连除非有明确需求否则不建议这么做。密码不要使用简单组合。数据库账号的密码强度容易被忽视但它在整个安全链条里的地位和服务器密码一样重要。建议用密码管理器生成随机串至少16位。权限可以细化到表和字段级别。比如GRANT SELECT (id, title) ON blog.posts TO ...只允许查询某几列。不过这个用法在业务系统里相对少见做精细化管控时才会用到。如果你用的是PostgreSQL思路完全一样只是语法略有差异CREATE DATABASE blog; CREATE USER blog_app WITH PASSWORD 这里是强密码; GRANT CONNECT ON DATABASE blog TO blog_app; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO blog_app;PostgreSQL里还多了一个schema级别的管理维度权限模型更精细一些但最小权限原则的落地思路是通用的。2.4 用角色收拢权限别让权限散落各处账号一多权限管理就会变乱。比如博客项目上线之后需要加一个读数据库的分析师账号如果每次都是手动CREATE USER然后把权限一条条写一遍很容易漏掉某张新表的授权。更好的做法是引入角色ROLE。角色就像权限的文件夹把一组权限打包然后把这个文件夹分配给需要的用户。后续新表建好了只要给角色补上权限所有拥有该角色的账号就自动获得权限不需要一个个去改。-- 创建角色 CREATE ROLE blog_readonly_role; -- 给角色授权 GRANT SELECT ON blog.* TO blog_readonly_role; -- 新用户分配角色 CREATE USER analyst01localhost IDENTIFIED BY 这里是强密码; GRANT blog_readonly_role TO analyst01localhost; -- 后续新表建好后只要执行一次 GRANT SELECT ON blog.new_table TO blog_readonly_role;这里有一个很多新手会遇到的问题给用户分配角色之后用户还需要激活角色才能生效。MySQL 8.0里可以用SET DEFAULT ROLE或者在登录时自动激活ALTER USER analyst01localhost DEFAULT ROLE blog_readonly_role;PostgreSQL里角色和用户的概念更统一用户在PostgreSQL里本身就是一个带LOGIN属性的角色所以用起来更顺滑CREATE ROLE blog_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO blog_readonly; GRANT blog_readonly TO analyst01;用角色的最大价值不是少写几条SQL而是让权限的变更有一个收敛点。权限管理最怕的就是权限散落在几十个不同的账号上别说审计连数都数不清。3. Python连接数据库时的权限相关报错排查权限设计做完了接下来的问题是怎么让Python应用顺畅地连上数据库。这一节把我见过的、跟权限直接相关的连接问题和解决办法集中整理一遍遇到报错可以按表自查。3.1 三类最常见的权限连接报错报错一Access denied for user xxxlocalhost (using password: YES/NO)这应该是最常见的权限报错。看到这个报错第一反应是检查三点用户名是不是拼写错了。密码是不是复制对了注意别把空格带进去。这个用户是否允许从当前主机连接。第三点最容易忽略。MySQL里CREATE USER blog_applocalhost这个账号只允许从本机连接。如果你从另一台服务器通过Python去连客户端IP是那台服务器的IP自然会被拒绝。这时候需要额外创建允许从指定IP连接的账号CREATE USER blog_app192.168.1.100 IDENTIFIED BY 这里是强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON blog.* TO blog_app192.168.1.100;报错二Host x.x.x.x is not allowed to connect to this MySQL server这个报错和上面是同一类问题的不同表现。MySQL在TCP握手阶段就会做主机校验不通过的IP直接拒掉。解决办法也是一致的把应用服务器的IP加进账号主机列表。报错三The user specified as a definer (xxx%) does not exist这个报错在数据库迁移或恢复场景里很常见根因是存储过程、视图或触发器里记录的definer账号在目标库里不存在。处理方式有两种一是创建对应的账号二是用ALTER DEFINER修改这些对象的definer。对于大多数Python应用场景实际遇到这个报错的概率不高但知道原因很重要它说明数据库对象和账号之间是存在绑定关系的删账号之前要先确认有没有对象依赖它。3.2 从连接参数角度减少权限踩坑除了数据库侧的权限配置Python这一侧的连接参数也有很多细节影响权限使用。用pymysql举例一个标准的连接配置长这样import pymysql conn pymysql.connect( host127.0.0.1, userblog_app, password这里是强密码, databaseblog, charsetutf8mb4, connect_timeout5, )有几个参数值得展开说connect_timeout是连接超时时间。很多人不设置默认值可能长达数分钟数据库不可达时Python程序会长时间卡在连接阶段。设置成5秒或更短能让错误快速暴露。charsetutf8mb4对于中文项目几乎是必须的这个设置解决了emoji和生僻字存储乱码的问题。如果你的Python代码和数据库在同一台机器优先用localhost或127.0.0.1而不是公网IP从网络层面减少暴露面。3.3 连接密码的管理从硬编码到环境变量连接参数里最容易出安全问题的就是密码。把密码直接写在.py文件里等于把钥匙挂在门口。一旦代码被上传到GitHub、被同事拷贝、被部署到共享服务器密码就脱离了你的控制。正确的做法是使用环境变量。把密码从代码里摘出来放到程序运行环境里import os import pymysql conn pymysql.connect( hostos.environ.get(DB_HOST, 127.0.0.1), useros.environ.get(DB_USER, blog_app), passwordos.environ.get(DB_PASSWORD, ), databaseos.environ.get(DB_NAME, blog), charsetutf8mb4, connect_timeout5, )运行程序之前在系统环境变量或启动脚本里设置这些值export DB_HOST127.0.0.1 export DB_USERblog_app export DB_PASSWORD这里是强密码 export DB_NAMEblog python app.py复杂的部署环境里可以用.env文件配合python-dotenv库来管理但.env文件必须加入.gitignore确保不会提交到代码仓库。这个细节我在帮人检查代码时反复强调过但还是经常看到把.env提交到仓库里的。3.4 连接池场景下的权限变更问题Python应用一旦上了并发通常会引入连接池技术。连接池的好处是复用数据库连接、减少握手开销但也会带来一个权限相关的问题管理员在数据库侧修改了某个账号的权限但连接池里已经存在的连接不会自动更新权限仍然按照连接建立时的权限运行。这个问题在快速迭代的项目里很常见。比如线上应用连接池里有50条连接你把某个账号的权限从可写改成了只读期望业务立刻变成只读模式但实际效果可能是过了一段时间才生效或者一直不生效直到连接被回收重建。解决办法有几种在管理后台手动杀掉旧连接MySQL里可以KILL对应的线程ID调整连接池的空闲回收时间在发布流程里加入数据库维护窗口发布期间重启应用服务。不需要所有场景都处理但要知道连接池的存在会延迟权限变更生效。4. SQL注入Python安全编程中最关键的防线权限管好了接下来是Python代码层面最重要的一课SQL注入的防御。这是安全编程里最经典、也最容易犯错的地方。4.1 字符串拼接为什么会出问题很多教材讲SQL注入会从攻击者的角度演示如何绕过登录我换个角度从开发者的视角来看这个问题。假设登录验证的代码是这样写的# 危险的写法不要这样写 username request.form[username] password request.form[password] sql SELECT * FROM users WHERE username username AND password password cursor.execute(sql)这段代码的问题是用户输入被直接拼进了SQL字符串。如果用户输入的username里含有SQL语法相关的特殊字符它就会成为SQL语句的一部分而不是单纯的数据。经典的万能密码就是利用了这一点让WHERE条件恒为真从而绕过身份验证。这里我想特别强调一个视角学会识别不安全的写法不等于要去攻击别人而是为了在自己的代码里消灭这类隐患。安全编程的本质是防御理解攻击原理是为了知道敌人会从哪里进来从而把入口堵死。4.2 参数化查询的正确姿势修复方式不是靠过滤特殊字符那样很容易遗漏边界情况。正确的做法是使用参数化查询Parameterized Query。参数化查询的核心思想是SQL语句的结构和参数数据彻底分离数据库引擎在解析SQL语句时只解析语句骨架参数以数据的形式单独传递不参与语法解析。用pymysql时这样写import pymysql conn pymysql.connect( host127.0.0.1, userblog_app, password这里是强密码, databaseblog, charsetutf8mb4, ) username request.form[username] password request.form[password] with conn.cursor() as cursor: sql SELECT * FROM users WHERE username %s AND password %s cursor.execute(sql, (username, password)) result cursor.fetchone()这里%s是占位符cursor.execute的第二个参数是一个元组元组里放的是真正的数据值。pymysql会把这两个参数安全地交给数据库驱动处理任何输入里的单引号、双引号、注释符都不会破坏SQL语句结构。如果是PostgreSQL psycopg2写法几乎一样占位符也是%simport psycopg2 conn psycopg2.connect( host127.0.0.1, userblog_app, password这里是强密码, dbnameblog, ) username request.form[username] password request.form[password] with conn.cursor() as cursor: sql SELECT * FROM users WHERE username %s AND password %s cursor.execute(sql, (username, password)) result cursor.fetchone()4.3 ORM不是免死金牌现在很多Python项目直接用SQLAlchemy、Django ORM这类工具操作数据库ORM会默认使用参数化方式执行查询这让很多开发者觉得用了ORM就安全了。这个想法有一半是对的但要分清情况。正常使用ORM的查询接口确实安全因为框架会自动参数化。问题出在以下几种场景用了text()写原生SQL又把外部输入直接拼接进去用了order_by或limit这类不能参数化的部分把用户输入直接拼进SQL片段在复杂统计报表中为了图省事直接f-string拼接表名和字段名。表名、字段名这类标识符是不能用参数占位符的这是SQL协议本身的限制。但这类需求也可以通过白名单机制解决比如预先定义允许排序的字段列表用户传入的排序字段必须在白名单里否则取默认值allowed_order_by { created_at: created_at, updated_at: updated_at, title: title, } order_col allowed_order_by.get(request.args.get(order_by), created_at) direction ASC if request.args.get(direction) ! DESC else DESC sql fSELECT * FROM posts ORDER BY {order_col} {direction}这个方案的精髓在于用户输入不直接进入SQL片段而是通过一个映射关系转化为预定义的值。即使传入的是恶意字符也只会得到默认的created_at数据库是安全的。4.4 用实际例子验证参数化的效果我在写这篇文章的时候顺手做了一个小实验用来演示参数化查询的防御效果。以下是实验的核心代码import pymysql conn pymysql.connect( host127.0.0.1, userblog_app, password这里是强密码, databaseblog, charsetutf8mb4, ) # 用户输入故意带入特殊字符 user_input abc OR 11 with conn.cursor() as cursor: sql SELECT * FROM users WHERE username %s cursor.execute(sql, (user_input,)) result cursor.fetchall() print(f查询返回 {len(result)} 条记录)把user_input故意设置成abc OR 11参数化查询会把它当成一个完整的字符串去匹配username字段而不是把它当作SQL逻辑的一部分。实验结果就是查到0条记录因为根本不存在用户名叫这个的用户。如果换成字符串拼接写法这个输入就会改变SQL的语义返回表里的所有用户。这就是参数化查询的价值用户输入再怎么花哨在数据库眼里它都是数据不是逻辑。5. 项目实战一个小型博客系统的权限与安全落地把前几节的内容放在一个真实项目里串起来才能看出这套方案怎么配合。这里我用一个常见的小型博客系统作为案例完整走一遍从建库到Python接入的过程。5.1 项目背景与账号设计场景设定一个多人写作的博客平台有读者浏览、作者发文、管理员管理三类角色。运营团队需要导出文章数据做分析。基于这些业务场景数据库账号设计如下账号用途权限范围备注blog_appWeb服务主账号blog库的SELECT/INSERT/UPDATE/DELETE覆盖读者和作者操作blog_report运营分析账号blog库的SELECT只读只能查blog_migrate表结构变更账号blog库的DDL权限只有发布时使用业务层面虽然分了读者、作者、管理员但商业数据层面它们对数据的操作最终都是增删改查所以用一个读写账号就能覆盖。如果项目体量更大可以考虑按业务模块继续拆分账号进一步缩小权限爆炸半径。5.2 从建库到授权的完整SQL-- 1. 建库 CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8mb4; -- 2. 建表使用迁移账号执行这类DDL USE blog; CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(100) NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS posts ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, title VARCHAR(200) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), FOREIGN KEY (user_id) REFERENCES users(id) ); -- 3. 创建业务账号 CREATE USER blog_applocalhost IDENTIFIED BY 这里用强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON blog.* TO blog_applocalhost; -- 4. 创建只读分析账号 CREATE USER blog_reportlocalhost IDENTIFIED BY 这里用另一个强密码; GRANT SELECT ON blog.* TO blog_reportlocalhost; -- 5. 创建结构迁移账号 CREATE USER blog_migratelocalhost IDENTIFIED BY 这里用第三个强密码; GRANT CREATE, ALTER, DROP, INDEX, REFERENCES ON blog.* TO blog_migratelocalhost; -- 6. 刷新权限MySQL 5.x 需要8.0 里授权即时生效但写上无妨 FLUSH PRIVILEGES;几个注意点建表和迁移操作都建议使用blog_migrate账号不要在应用代码里执行DDLblog_app只有增删改查权限就算代码被注入攻击者也改不了表结构blog_report只读账号用于数据分析防止报表脚本误操作污染数据。5.3 Python端接入从环境变量到参数化CRUD项目的config.py里不写任何密码相关的东西只负责从环境变量读取import os DB_CONFIG { host: os.environ.get(DB_HOST, 127.0.0.1), user: os.environ.get(DB_USER, blog_app), password: os.environ.get(DB_PASSWORD, ), database: os.environ.get(DB_NAME, blog), charset: utf8mb4, connect_timeout: 5, }写一个简单的数据访问层import pymysql from config import DB_CONFIG class PostRepository: 文章数据访问类只使用参数化查询 def __init__(self): self.config DB_CONFIG def get_recent_posts(self, limit10): with pymysql.connect(**self.config) as conn: with conn.cursor() as cursor: sql SELECT id, title, user_id, created_at FROM posts ORDER BY created_at DESC LIMIT %s cursor.execute(sql, (limit,)) return cursor.fetchall() def create_post(self, user_id, title, content): with pymysql.connect(**self.config) as conn: with conn.cursor() as cursor: sql INSERT INTO posts (user_id, title, content) VALUES (%s, %s, %s) cursor.execute(sql, (user_id, title, content)) conn.commit() return cursor.lastrowid这里想说明一个容易被忽视的细节LIMIT %s也是参数化的一个典型用法。很多人以为只有WHERE条件里才需要参数化但实际上任何外部输入进入SQL都应该是参数化方式。占位符在pymysql和psycopg2里不仅能用在值的位置也可以用在LIMIT这类子句中非常实用。5.4 验证权限落地效果项目跑起来之后可以用命令行的方式验证权限控制是否生效。验证一用blog_report账号尝试写入应该被拒绝mysql -ublog_report -p -h127.0.0.1 blog然后在MySQL命令行里执行INSERT INTO posts (user_id, title, content) VALUES (1, test, test);预期结果是收到权限不足的报错说明只读账号的权限控制生效了。验证二用blog_app账号尝试删表也应该被拒绝DROP TABLE posts;预期结果是DROP命令不被允许因为blog_app没有DDL权限。我建议所有读者在本地把这个验证过程真实跑一遍。这个验证的心理学价值比技术价值还大亲眼看到权限拦截生效你对最小权限原则的理解会从纸面概念变成肌肉记忆。以后写代码的时候你会下意识问自己一句这个账号到底该不该有这个权限6. 在今天的学习之后我建议你做这样几件事说完了原理和案例最后分享一些我在实际项目里总结的经验和踩坑心得以及给Day40之后的学习建议。6.1 动手做一次代码体检我强烈建议你把自己写过的最大的那个Python项目翻出来回答下面几个问题检查点检查方法通过标准数据库账号查看数据库连接配置不使用root有独立的业务账号密码管理检查源码和配置文件无硬编码密码通过环境变量管理SQL写法全局搜索execute、query等调用无字符串拼接SQL全部参数化ORM裸SQL搜索text()、raw()等关键字无外部输入直接拼接数据库权限查询账号授权记录每个账号权限最小化代码仓库检查.gitignore.env、配置文件未提交这份清单看起来简单但真做起来你会发现很多项目一项都过不了。我帮人做代码评审的时候最常见的结果就是一页纸的问题列表。正常不用慌发现问题、改掉问题就是这一天的收获。6.2 三个最容易颠覆认知的安全真相第一个真相用ORM不等于防住了SQL注入。我在前面已经解释过但如果要用一句话记住就是ORM只在底层正确地参数化如果你自己拼了字符串再塞给ORMORM也救不了你。第二个真相很多真实攻击来自内部而非外部。开发团队里某个人误操作、测试脚本连错了生产库、离职员工的账号没有及时回收这些场景在真实项目中发生的概率远高于一次外部黑客攻击。权限管理不仅仅是为了防黑客更是为了给团队协作中的人为失误加上一道缓冲。第三个真相数据库密码不是唯一的安全措施但它是最后一道防线。应用层的输入校验、Web层的防火墙、操作系统层面的安全配置都很重要但一旦所有外层防线都被攻破数据库权限就是最后一个可以拦住破坏的地方。做好它相当于给你的数据资产上了最后一道保险。6.3 我踩过的坑和最后一点建议我在学习阶段踩得最深的一个坑是在本地开发环境用root账号连数据库跑了好几个月养成了习惯。后来第一次做真正的线上部署连接配置全部照搬把生产环境的密码和root账号都写在代码里推到了仓库上。第二天就收到了安全扫描工具的邮件提醒我仓库里检测到了数据库密码。那次幸好发现得早数据库没有出事但那种心惊肉跳的感觉我到现在还记得。从那以后我养成了一个习惯任何项目的第一步先建独立的数据库账号再开始写代码。这个习惯的成本很低只是多写几条SQL但它带来的回报是持续性的——你永远不会在项目里留下一个需要回头补救的权限漏洞。Day40这个节点对新程序员来说是个分水岭。前面39天学的是怎么让代码跑起来从今天开始要学的是怎么在真实环境里让代码安全、稳定、可控地跑。SQL权限管理和Python安全编程就是这条路上绕不开的第一个关卡。建议你把今天的内容在自己的项目里实操一遍然后把踩坑记录发到评论区大家一起讨论一起进步。
返回列表