🛢️ 第20章 权限管理及安全控制
📄 一句话概括

账号是「用户名加来源」:'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 秒实验

  1. 给 reader 加一行「只能查 name 和 price 两列」的权限(ACL 表怎么表示?先想一遍再动手)。
  2. 模拟一次改名:把 app 改成 app2,再跑一次 acl(),看结果。
  3. 写一段代码,把某个账号的权限全部删掉,再对比「逐个 REVOKE」和「DROP USER」的差别。

常见误区

  1. 以为 'app'@'localhost' 和 'app'@'%' 是同一个账号。它们是两个账号,权限各算各的。 这是「授权了却连不上」的头号原因。
  2. 以为 DROP USER 之前要先 REVOKE。删号会连带删权限。 先 REVOKE 是为了「留号收权」,不是删号的前置步骤。
  3. 随手给 WITH GRANT OPTION。它等于让对方也能授权,会形成追不回来的授权链。
自测三问(点开对答案) 1. `'app'@'localhost'` 和 `'app'@'%'` 有什么区别? —— 是两个不同的账号。前者只认本机,后者任何来源都能连。 权限分别计算,给一个授了不等于另一个也有。 2. `DROP USER` 和 `REVOKE` 该在什么场景用? —— `REVOKE` 是「留号收权」,账号还在,恢复时一条 `GRANT` 就回来; `DROP USER` 是「删号」,连带删掉所有权限和密码,恢复要重建。临时的用前者,永久的用后者。 3. `WITH GRANT OPTION` 为什么危险? —— 拿到它的人可以把权限再授给别人,形成一条授权链。出问题时很难追——你不知道中间转了几手。 它本质是「开门的权力」,比「进门」更危险。

命令讲完了。下一篇讲三个常见问题:权限什么时候生效、密码怎么存才安全。

相关笔记