CREATE USER 'u'@'%' IDENTIFIED BY '密码',
ALTER USER 改密码,DROP USER 删账号。 create user 只建账号、不给任何权限,
权限要另外 grant。
drop user 只删账号,不删它建过的数据——账号是「谁」,数据是「什么」,两者独立。
本沙箱没有密码,只有「谁能读写这个文件」:
只读用 ?mode=ro,临时锁写用 pragma query_only。
上一篇把安全模型的三层讲清了,这一篇只讲第一层:账号。 这一篇在本沙箱里会变成「一个用户一个库文件」—— 这不是凑数,而是嵌入式数据库做多租户的真实做法。 理解了它,也就理解了「数据库权限」和「文件权限」的边界在哪。
一、建、改、删:三句话
建:create user 'report'@'%' identified by 'Str0ng!pwd';
改密码:
alter user 'report'@'%' identified by 'NewPwd!2026';
(也可以写 set password for 'report'@'%' = '新密码';)
删:drop user 'report'@'%';
create user 只建账号,不给任何权限。 新建的账号连 select 都做不了,
权限要另外 grant(下一篇)。
这个「默认无权限」的设计是对的:忘了给权限,功能用不了;忘了收权限,数据就漏了。
二、identified by 后面是明文,存的不是
identified by 后面写明文密码,
数据库会自己哈希后存进 mysql.user。 所以「密码有没有加密」不用你操心。
要操心的是「密码强度」:
MySQL 8 有 validate_password 插件,可以强制长度、大小写、数字、符号。
默认策略在不少发行版上是「中等」,
所以会见到 ERROR 1819 这类「密码太弱」的报错——那不是 bug,是策略在拦。
还有一条:MySQL 8 默认认证插件是 caching_sha2_password,
老客户端(比如某些版本的 Navicat、老驱动)不支持它,
连上去会报「认证插件不支持」。 解法是建账号时指定 with mysql_native_password,
或者升级客户端。
三、drop user 不删数据
drop user 只删账号,不删它建过的数据。 这一点和 drop table 不一样——
账号是「谁」,数据是「什么」,两者独立。
所以「某人离职了,删账号」是安全的,但「某个库没人用了,删库」才是删数据。
两件事要分开想。
反过来说,删账号之前要想清楚:这个账号是不是某个定时任务在用?
删掉之后任务会静默失败(连接报错),
而失败告警未必有人看。 这一条和第 13.5 篇「定时任务悄悄不跑了」是同一类事故。
四、本沙箱的「多租户」:一个用户一个文件
没有账号,怎么区分用户? 靠「一个用户一个库文件」。
Demo 里造 a.db、b.db 两个文件,各存各的笔记。
两个库各自是完整的数据库,谁也看不见谁。 这就是这里的多租户——用文件隔离,不用账号隔离。
好处是「删一个租户」就是删一个文件,干净利落;
坏处有两个:一是「跨租户统计」特别麻烦(要 attach 好几个文件);
二是「租户多了之后文件一大把」,备份、迁移、监控都得自己写循环。
五、改密码这件事,在这里变成改文件权限
MySQL 改密码是 alter user … identified by …。 这里没有密码,只有「谁能读写这个文件」。
于是「改密码」就变成了「改文件权限」——
Linux 上 chmod 600 app.db,Windows 上用文件属性的「安全」页。
600 的意思是「只有属主能读写」,
这是数据库文件该有的权限。 见过把 .db 放在网站根目录、权限 644 的——
浏览器直接 GET /app.db 就把整个库下载走了。
本沙箱没法真演示 chmod(Windows 上语义不同),
但「别把库文件放在能被直接下载的位置」这一条,跟操作系统无关。
六、只读连接:一条 URL 参数就是一个权限
sqlite3.connect('file:a.db?mode=ro', uri=True)——注意 uri=True 是必须的,
不加它 ?mode=ro 会被当成文件名的一部分。
读得到,写不了,报 attempt to write a readonly database。 建表也一样被拦。
关键在「它不是建议,是打开方式」:文件是按只读方式打开的,
所以任何写操作在文件层面就失败了——绕过不了、也没有开关能临时打开。
mode 还有两个值:rw(读写,文件必须已存在)、
rwc(读写,不存在就建,这是默认值)。
用 rw 能顺手防住「路径写错时静默建了个空库」这个坑——
那个坑会让你以为「数据全丢了」,其实是打开了另一个文件。
七、query_only:连接级的只读开关
pragma query_only = on 之后,这条连接上的写操作全被拦,报错和只读连接一样。
它和 mode=ro 的区别在「什么时候生效」:mode=ro 是打开文件时就只读,改不了;
query_only 是连接上的一个开关,随时能 off。
所以它是「防手滑」,不是「防越权」。
适合的场景是「这个函数只该读,我把它锁一下,免得自己写错」;
不适合的场景是「防止别人拿到这个连接干坏事」。
选哪个,取决于你要防的是「自己」还是「别人」。
八、动手 Demo:两个库文件当两个租户,再用只读连接管住写
Demo 五段:造两个库文件模拟两个租户、用 ?mode=ro 打开一个试写、
用 pragma query_only 锁住另一条连接、最后再撞一次账号语句。
第二段和第三段的报错文字一模一样(attempt to write a readonly database),
但背后的机制不同:一个在文件层,一个在连接层。
import sqlite3
import _q46 as Q
c = Q.con()
# 1) 「多租户」:一个用户一个库文件
a = sqlite3.connect("a.db")
b = sqlite3.connect("b.db")
for conn, who in ((a, "A"), (b, "B")):
conn.execute("CREATE TABLE note (id INTEGER PRIMARY KEY, txt TEXT)")
conn.execute("INSERT INTO note (txt) VALUES (?)", (who + " 的笔记",))
conn.commit()
print(a.execute("SELECT * FROM note").fetchall()) # 只有 A 的
print(b.execute("SELECT * FROM note").fetchall()) # 只有 B 的
# 2) 只读连接:一条 URL 参数就是一个权限
ro = sqlite3.connect("file:a.db?mode=ro", uri=True)
Q.show(ro, "SELECT id, txt FROM note")
ro.execute("INSERT INTO note (txt) VALUES ('写一个试试')")
ro.execute("CREATE TABLE tmp (x INT)")
# 3) query_only:连接上的只读开关
c.execute("PRAGMA query_only = ON")
Q.show(c, "PRAGMA query_only")
c.execute("DELETE FROM students")
# 4) 账号语句仍然全不认
for s in ("CREATE USER 'x'@'%'", "DROP USER 'x'@'%'"):
try:
c.execute(s)
except Exception as e:
print(s, "->", e)
实跑输出:
--------------------------------------------------------------------
第七十六课:用户管理——建账号、改密码、删账号
--------------------------------------------------------------------
MySQL 里建账号的三句话
MySQL 原码(本沙箱不执行):
| CREATE USER 'report'@'%' IDENTIFIED BY 'Str0ng!pwd';
| ALTER USER 'report'@'%' IDENTIFIED BY 'NewPwd!2026';
| DROP USER 'report'@'%';
**改密码也可以写 `SET PASSWORD FOR 'report'@'%' = '新密码';`**
**`create user` 只建账号,不给任何权限。** **新建的账号连 `select` 都做不了**——
**权限要另外 `grant`**(下一篇)。
**这个「默认无权限」的设计是对的:忘了给权限,功能用不了;忘了收权限,数据就漏了。**
**`identified by` 后面是明文密码**,
**数据库会自己哈希后存进 `mysql.user`。** **所以「密码有没有加密」这件事不用你操心**,
**但「密码强度」要操心**——**MySQL 8 有 `validate_password` 插件,可以强制长度和复杂度。**
**`drop user` 只删账号,不删它建过的数据。** **这一点和 `drop table` 不一样**——
**账号是「谁」,数据是「什么」,两者独立。**
本沙箱:「用户」这个概念不存在
**没有 `create user`,那「区分用户」靠什么?** **靠「一个用户一个库文件」。**
**Demo 里造 A、B 两个文件,模拟两个租户。**
SQL> SELECT * FROM note
id txt
----------------
1 A 的笔记
(1 行)
SQL> SELECT * FROM note
id txt
----------------
1 B 的笔记
(1 行)
**两个库各自是完整的数据库,谁也看不见谁。** **这就是这里的「多租户」**——
**用文件隔离,不用账号隔离。**
**好处是「删一个租户」就是删一个文件**,干净利落;
**坏处是「跨租户统计」特别麻烦**(要 `attach` 好几个文件),
**而且「一个租户的数据量很大」时会有一堆文件要管。**
改密码这件事,在这里变成了「改文件权限」
**MySQL 改密码是 `alter user … identified by …`。** **这里没有密码,只有「谁能读写这个文件」。**
**于是「改密码」就变成了「改文件权限」**——**Windows 上用文件属性的「安全」页**,
**Linux 上 `chmod 600 app.db`。**
**`600` 的意思是「只有属主能读写」**,
**这是数据库文件该有的权限。** **见过把 `.db` 放在网站根目录、权限 644 的**——
**浏览器直接 `GET /app.db` 就把整个库下载走了。** **这一类事故每年都有。**
**本沙箱没法真演示 `chmod`**(Windows 上语义不同),
**但「别把库文件放在能被直接下载的位置」这一条,跟操作系统无关。**
只读连接:一条 URL 参数就是一个权限
SQL> SELECT id, name FROM students
id name
------------
1 张伟
2 李娜
3 王强
(3 行)
SQL> INSERT INTO students (name, height) VALUES ('赵敏', 168.0)
报错:attempt to write a readonly database
SQL> CREATE TABLE tmp (x INT)
报错:attempt to write a readonly database
**读得到,写不了**,报 `attempt to write a readonly database`。 **建表也一样被拦。**
**关键在连接 URL 那个 `?mode=ro`**:**它不是「建议只读」,是「打开时就按只读方式打开文件」**。
**所以任何写操作在文件层面就失败了**,**绕过不了、也没有开关能临时打开。**
**`mode` 还有两个值**:**`rw`(读写,文件必须已存在)**、
**`rwc`(读写,不存在就建,这是默认值)**。
**用 `rw` 能顺手防住「路径写错时静默建了个空库」这个坑。**
query_only:连接级的只读开关
SQL> PRAGMA query_only = ON
完成
SQL> PRAGMA query_only
query_only
-----------
1
(1 行)
SQL> SELECT COUNT(*) AS 人数 FROM students
人数
-----
3
(1 行)
SQL> DELETE FROM students
报错:attempt to write a readonly database
**`pragma query_only` 返回 1,删除被拦,报的也是 `attempt to write a readonly database`。**
**它和 `mode=ro` 的区别在「什么时候生效」**:**`mode=ro` 是打开文件时就只读,改不了**;
**`query_only` 是连接上的一个开关,随时能 `off`。** **所以它是「防手滑」,不是「防越权」**——
**适合的场景是「这个函数只该读,我把它锁一下,免得自己写错」。**
第一段两个库各一行:A 只看到「A 的笔记」,B 只看到「B 的笔记」。
第二段只读连接读得到、写不了,报 attempt to write a readonly database。
第三段 pragma query_only 返回 1,delete 同样被拦。
第四段账号语句依旧 syntax error。
30 秒实验
connect('file:a.db?mode=ro')忘了写uri=True会怎样?试一下看它报什么。- 把
mode=ro换成mode=rw,再删掉文件重跑,会报什么? - 为什么「删账号」要先去确认有没有定时任务在用?
常见误区
- 建完账号就去连。
create user不给权限,连select都做不了,要另外grant。 - 以为
drop user会删数据。 它只删账号,数据独立存在。 - 只读连接忘了
uri=True。?mode=ro会被当成文件名,静默建出一个奇怪名字的空库。
自测三问(点开对答案)
1. `create user` 之后新账号能 `select` 吗? —— 不能。它只建账号,不给任何权限。要给权限得另外 `grant`。 2. `drop user` 会删掉它建的表吗? —— 不会。账号和数据是两件事,删账号只是删「谁」,不影响「什么」。 3. `?mode=ro` 和 `pragma query_only` 有什么区别? —— `mode=ro` 在打开文件时就只读,改不了,是硬限制; `query_only` 是连接上的开关,随时能关,防手滑但不防越权。账号有了,下一篇给它们分配「能干什么」:16.3 权限管理——grant、revoke,以及这里的替身。
- ☐ (本篇完,下一篇 16.3 权限管理)