账号是「用户名加来源」:'app'@'localhost' 和 'app'@'%' 是两个账号。
GRANT 授的是「库.表」上的动作,粒度能细到列;WITH GRANT OPTION 等于造个小 root。
DROP USER 会连带删掉所有权限,所以「收权」和「删号」是两件事。
这一篇讲具体的四条命令。
教材 20.2 有四小节:CREATE USER、DROP USER、RENAME USER、GRANT 和 REVOKE。
四条命令本身都很短,真正要讲清的是三个容易踩的点:账号的来源部分、授权的粒度、
以及「收权」和「删号」的区别。
沙箱一条都不认,所以这一篇的结构是「贴原码 + 用 ACL 表演示等价效果」。
一、CREATE USER:账号是「用户名加来源」
CREATE USER 'app'@'localhost' IDENTIFIED BY 'Str0ng!Pass';
CREATE USER 'app'@'192.168.1.%' IDENTIFIED BY 'Str0ng!Pass';
CREATE USER 'read'@'%' IDENTIFIED BY 'Another!Pass';
教材没强调的是那个 @ 后面的部分:它不是「可选的备注」,是账号的一部分。
'app'@'localhost' 和 'app'@'%' 是两个不同的账号,权限也各算各的。
这一点经常导致「明明授权了却连不上」:授的是 @'localhost',而应用从内网 IP 连进来,
匹配的是另一个账号或者根本没匹配上。
| 写法 | 含义 |
|---|---|
@'localhost' |
只认本机(走 socket 或 127.0.0.1) |
@'192.168.1.%' |
这个网段的所有机器 |
@'%' |
任何来源 |
所以「谁能连」这件事,在 MySQL 里是由「账号加来源」一起决定的。
同一台机器上跑两个应用,想给不同权限,就得建两个账号——不能靠「同名不同权」。
密码那部分用 IDENTIFIED BY。教材后面 20.3 专门讲密码,这里只提一句:
IDENTIFIED BY 给的是明文,MySQL 会自己算成哈希存起来。
所以「配置文件里写明文密码」这件事,问题不在「会不会被 MySQL 存明文」,
在「配置文件本身会被谁看到」。
二、DROP USER 与 RENAME USER
DROP USER 'app'@'localhost';
RENAME USER 'app'@'localhost' TO 'app'@'192.168.1.%';
DROP USER 有个容易忽略的后果:它连带删掉这个账号的所有权限。
所以你不需要先 REVOKE 再 DROP——REVOKE 是为了「留着账号但收权」。
这个区别在运维上很实在:临时停一个应用,该 REVOKE 还是 DROP?
REVOKE 之后账号还在,恢复时一条 GRANT 就回来了;DROP 之后账号没了,得连密码一起重建。
「临时」和「永久」这两个词,在这种地方才显出分量。
RENAME USER 在 SQLite 里没有对应物,在 ACL 复刻里就是「把 user 列改个值」。
注意改名要检查重名——撞上已存在的账号会报错,而报错的方式各版本不完全一样。
改名这件事还有个隐蔽的坑:应用配置里写的是旧名字。
改完库不改配置,下次重启就连不上了。
所以改名前先查一遍「谁在用这个账号」,这一步和 18 章删事件前的「查引用」是同一件事。
三、GRANT:授的是「库.表」上的动作
GRANT SELECT, INSERT ON shop.tb_g TO 'app'@'localhost';
GRANT SELECT (name, price) ON shop.tb_g TO 'reader'@'localhost';
GRANT ALL PRIVILEGES ON shop.* TO 'boss'@'localhost' WITH GRANT OPTION;
三行的粒度一层比一层粗:表级、列级、库级。 还有一个第四个维度:能不能转授。
| 写法 | 粒度 | 典型场景 |
|---|---|---|
ON shop.tb_g |
表 | 应用账号只要这几张表 |
ON shop.tb_g (列) |
列 | 只让看手机号后四位 |
ON shop.* |
库 | 库管理员 |
WITH GRANT OPTION |
转授权 | 极少用,等于造了个小 root |
第四行要单独说:WITH GRANT OPTION 让被授权者可以把权限再授给别人。
它能形成一条「授权链」,出问题时很难追——因为你不知道中间转了几手。
所以实践里几乎不给人这个选项,宁可让 DBA 统一授。
「谁能授权」这件事本身就是一种权限,而且是最危险的那种。
因为它不改数据、不删表,只是「再开一扇门」——而门开多了,就没人知道总共有几扇。
四、REVOKE:收权的两个坑
REVOKE INSERT ON shop.tb_g FROM 'app'@'localhost';
REVOKE 的语法和 GRANT 对称,但效果有两个坑。
第一,只能收「授过的」——收一个没授过的权限会报错,而不是「无声通过」。
第二,收权不会断开已经连着的会话。这一点下一篇展开。
复刻版里,「收权」就是删掉 ACL 表里的那一行:
| 时刻 | app 手里的权限 |
|---|---|
| 收权之前 | SELECT、INSERT |
| 收权之后 | SELECT |
一行 DELETE,INSERT 就没了,SELECT 还在。
注意它「只删这一行」——不会连带把别的权限一起清掉。
这个「精确删除」的特性很重要:REVOKE 是按「表加动作」这一对来收的。
所以要收干净一个账号的权限,得把它手里的每一对都收一遍,或者干脆 DROP USER。
实践里更常见的做法是「按角色管理」:不直接给人授权,先建角色,人挂在角色上。
收权就是把人从角色里摘出来,一步到位。
五、动手 Demo:三条 GRANT 的粒度,加上一次 REVOKE
这一段贴三条 GRANT 和一条 REVOKE 的原码(沙箱都不执行),
然后用 ACL 表把「收权」的效果真跑一遍:收之前 SELECT 加 INSERT,收之后只剩 SELECT。
import sqlite3
c = sqlite3.connect(u"shop.db", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_acl (user TEXT, tbl TEXT, act TEXT,
PRIMARY KEY (user, tbl, act));
""")
c.executemany(u"INSERT INTO tb_acl VALUES (?,?,?)", [
(u"app", u"tb_g", u"SELECT"),
(u"app", u"tb_g", u"INSERT"),
(u"boss", u"tb_g", u"SELECT"),
])
def acl(user):
return sorted(r[0] for r in c.execute(
u"SELECT act FROM tb_acl WHERE user=?", (user,)))
print(u"收权之前", acl(u"app")) # ['INSERT', 'SELECT']
c.execute(u"DELETE FROM tb_acl WHERE user='app' AND act='INSERT'")
print(u"收权之后", acl(u"app")) # ['SELECT']
实跑输出:
--------------------------------------------------------------------
用户和权限管理:建、删、改名、授权
--------------------------------------------------------------------
CREATE USER:账号是「用户名加来源」
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| CREATE USER 'app'@'localhost' IDENTIFIED BY 'Str0ng!Pass';
| CREATE USER 'app'@'192.168.1.%' IDENTIFIED BY 'Str0ng!Pass';
| CREATE USER 'read'@'%' IDENTIFIED BY 'Another!Pass';
沙箱不认,六条权限语句全是 syntax error。
**教材没强调的是那个 `@` 后面的部分:它不是「可选的备注」,是账号的一部分。**
**`'app'@'localhost'` 和 `'app'@'%'` 是两个不同的账号,权限也各算各的。**
**这一点经常导致「明明授权了却连不上」:授的是 `@'localhost'`,而应用从内网 IP 连进来,**
**匹配的是 `@'%'` 或者根本没匹配上。**
**`@'%'` 表示「任何来源」,`@'192.168.1.%'` 表示「这个网段」,`@'localhost'` 只认本机。**
**所以「谁能连」这件事,在 MySQL 里是由「账号加来源」一起决定的。**
DROP 与 RENAME:两条都是一句话
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| DROP USER 'app'@'localhost';
| RENAME USER 'app'@'localhost' TO 'app'@'192.168.1.%';
沙箱两条都不认:`near "USER"`、`near "RENAME"`。
**`DROP USER` 有个容易忽略的后果:它连带删掉这个账号的所有权限。**
**所以你不需要先 `REVOKE` 再 `DROP`——`REVOKE` 是为了「留着账号但收权」。**
**`RENAME USER` 在 SQLite 里没有对应物,在 ACL 复刻里就是「把三列里的 user 改个值」。**
**注意改完之后要检查「有没有重名」——改名撞上已存在的账号会报错。**
GRANT:授的是「库.表」上的动作
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| GRANT SELECT, INSERT ON shop.tb_g TO 'app'@'localhost';
| GRANT SELECT (name, price) ON shop.tb_g TO 'reader'@'localhost';
| GRANT ALL PRIVILEGES ON shop.* TO 'boss'@'localhost' WITH GRANT OPTION;
| REVOKE INSERT ON shop.tb_g FROM 'app'@'localhost';
沙箱不认 `GRANT`/`REVOKE`,报 `near "GRANT"`。
**四行的粒度一层比一层细:表级、列级、库级、以及「能不能转授」。**
写法 粒度 典型场景
------------------------------------------------------
`ON shop.tb_g` 表 应用账号只要这几张表
`ON shop.tb_g (列)` 列 只让看手机号后四位
`ON shop.*` 库 库管理员
`WITH GRANT OPTION` 转授权 极少用,等于造了个小 root
**第四行要单独说:`WITH GRANT OPTION` 让被授权者可以把这个权限再授给别人。**
**它能形成一条「授权链」,出问题时很难追——因为你不知道中间转了几手。**
**所以实践里几乎不给人这个选项,宁可让 DBA 统一授。**
REVOKE:收权比授权更需要小心
--------------------------------------------------------------------
**`REVOKE` 的语法和 `GRANT` 对称,但它的效果有两个坑。**
**第一,只能收「授过的」——收一个没授过的权限会报错。**
**第二,收权不会断开已经连着的会话(这一点下一篇展开)。**
**复刻版里,「收权」就是删掉 ACL 表里的那一行。**
**删之前先查一下「谁会有影响」,删之后再看一眼结果——这两步和 18 章删事件的纪律一样。**
时刻 app 手里的权限
------------------------
收权之前 INSERT、SELECT
收权之后 SELECT
**一行 `DELETE`,`INSERT` 就没了,`SELECT` 还在。**
**这就是 `REVOKE INSERT ON shop.tb_g FROM 'app'@'localhost'` 的全部效果。**
**注意它「只删这一行」——不会连带把别的权限一起清掉。**
两行输出的差别就是 REVOKE 的全部效果。
注意 boss 那一行没被动过——REVOKE 是「针对某个账号的某个动作」,
不是「清空某张表的权限」。
这也是为什么收权要写清楚「对谁、在哪张表上、收回哪个动作」,三个都得有。
少写一个,要么报错,要么收错了人。
顺带说一个实践中的习惯:收权之前先 SHOW GRANTS FOR 'app'@'localhost' 看一眼。
看一眼再动手,和直接动手再回头查,成本差十倍。
30 秒实验
- 给
reader加一行「只能查name和price两列」的权限(ACL 表怎么表示?先想一遍再动手)。 - 模拟一次改名:把
app改成app2,再跑一次acl(),看结果。 - 写一段代码,把某个账号的权限全部删掉,再对比「逐个 REVOKE」和「DROP USER」的差别。
常见误区
- 以为
'app'@'localhost'和'app'@'%'是同一个账号。它们是两个账号,权限各算各的。 这是「授权了却连不上」的头号原因。 - 以为
DROP USER之前要先REVOKE。删号会连带删权限。 先REVOKE是为了「留号收权」,不是删号的前置步骤。 - 随手给
WITH GRANT OPTION。它等于让对方也能授权,会形成追不回来的授权链。
自测三问(点开对答案)
1. `'app'@'localhost'` 和 `'app'@'%'` 有什么区别? —— 是两个不同的账号。前者只认本机,后者任何来源都能连。 权限分别计算,给一个授了不等于另一个也有。 2. `DROP USER` 和 `REVOKE` 该在什么场景用? —— `REVOKE` 是「留号收权」,账号还在,恢复时一条 `GRANT` 就回来; `DROP USER` 是「删号」,连带删掉所有权限和密码,恢复要重建。临时的用前者,永久的用后者。 3. `WITH GRANT OPTION` 为什么危险? —— 拿到它的人可以把权限再授给别人,形成一条授权链。出问题时很难追——你不知道中间转了几手。 它本质是「开门的权力」,比「进门」更危险。命令讲完了。下一篇讲三个常见问题:权限什么时候生效、密码怎么存才安全。
- ☐ (本篇完,下一篇 20.3 MySQL 数据库安全常见问题)