GRANT 权限 ON 库.表 TO '账号',REVOKE 权限 ON 库.表 FROM '账号',
SHOW GRANTS FOR '账号' 复查。
三种范围:
*.* 全局、库.* 库级、库.表 表级。
with grant option 是「允许他再授权给别人」,要慎给。
本沙箱用 set_authorizer 做等价物:
每条语句执行前先问一句「允许吗」,拒绝就报 not authorized。
上一篇讲「谁」,这一篇讲「能干什么」——这是安全模型里最核心的一层。
本沙箱的替身 set_authorizer 能真跑,而且能力不弱:语句级、表级、列级都能拦,
只是不能按行、也不算安全边界。 这两条边界讲清了,这一篇就到位了。
一、授权与收权:grant、revoke
grant 权限 on 库.表 to 账号,revoke 权限 on 库.表 from 账号。
权限名是一串关键字:select、insert、update、delete、create、drop、alter、
index、execute、all privileges。
可以一次给多个:
grant select, insert on shop.* to 'app'@'%'; 给多了就用 revoke 收,两项要能对上。
revoke 的粒度不能比 grant 更细,给了库级就收库级,不能只收某张表。
复查一定要用 show grants for '账号',别靠记忆。
权限是会随时间积累的:
三年前给 select、两年前加了 update、去年又加了 delete——
当初的理由早忘了,权限还留着。
二、三种范围与 with grant option
on *.* 是全局(所有库所有表);on shop.* 是库级;on shop.orders 是表级。
还有一种列级:grant select (name, price) on shop.goods,只让看几列。
with grant option 是「允许他再授权给别人」。 这个要慎给:
拿到它的人等于可以自己复制权限,你收回来的时候,他授出去的那些还在。
所以「谁能授权」这件事本身就要收紧,
一般只有管理员账号带这个选项。 revoke 收权限时如果要连「转授出去的」一起收,
得写 revoke … cascade(MySQL 里行为随版本有差异,用前先查文档)。
三、flush privileges 多数时候是仪式
这句话老出现在教程里,它的作用是「重新加载权限表到内存」。
用 grant、revoke、create user 时数据库会自己刷,不用你管;
只有直接 update mysql.user 改表的时候才需要——
而那种做法本来就不该用(改错了没有语法检查)。
所以这句话多数时候是「抄来的仪式」,不加也行。 但它有一个正当用途:
你手动 insert into mysql.user 造了账号之后,不刷就不生效。
只是这条路早就被 create user 取代了。
四、这里没有 grant,但有 set_authorizer
set_authorizer 是 sqlite3 的一个回调:
每一条语句在执行前,都会先问一句「这个动作允许吗」。
你返回「允许」它就做,返回「拒绝」它就报 not authorized。
Demo 里只放行 select 和 read,其余全拒。 结果:
查得到,写、建表、删表全报 not authorized。
这和 grant select 的效果是一样的——
只不过一个在数据库里,一个在应用进程里。
这也正好说明了「权限」这个概念的通用性:不管在哪一层实现,都是「动作 + 对象 → 允许/拒绝」。
五、表级白名单:把一张表藏起来
Demo 第二段换了个回调:
students 放行,salary 一律拒。
结果 students 能查,salary 报 access to salary.pay is prohibited。
注意报错里带了「哪一列」——因为读取的粒度是「表 + 列」。 所以列级权限也能做:
同一张表,name 能读、pay 不能读。
这就相当于「表级权限」甚至「列级权限」:
同一张库文件里,有的表能看、有的表不能看;同一张表里,有的列能看、有的列不能看。
这一点比 mode=ro 强——只读连接是「全部不能写」,这里是「有的能读、有的不能读」。
六、authorizer 的两条边界
第一,它做不到按行控制。 它只能拦「读 salary 表」,不能拦「读 salary 表里张三那行」。
按行的权限,MySQL 也没有(那是视图 + where 的事,第 11 章)。
第二,它不是安全边界。 authorizer 跑在你的进程里,是你的代码在拦你自己。
能改你代码的人,把回调删掉就绕过去了。 真正的安全边界是文件权限和「别让不该连的人连上」。
那它有什么用? 防手滑、防注入。
比如一个「只该读」的报表模块,挂上只读 authorizer 之后,
就算拼接 SQL 被注入了 drop table,也执行不了。 这一层的价值就在这儿。
七、最小权限,落到具体做法
MySQL 上的做法:一个应用一个账号(不要所有人共用一个 root)、
只给需要的库和需要的动作、只读模块单独一个账号、定期用 show grants 复查一遍。
本沙箱上的做法:只读模块用 ?mode=ro 打开、需要更细的规则就挂 authorizer、
库文件放在不能被直接下载的位置、多租户就一个租户一个文件。
两条路的思路是一样的:
不是「相信代码不会写错」,而是「写错了也造不成大祸」。 权限就是那道兜底——
它平时看不出价值,出事那天能救命。
八、动手 Demo:用 authorizer 当 grant:只读、表级白名单、再关掉
Demo 三段:挂一个「只放行读」的回调,看写和 DDL 全被拒;换成「表级白名单」把 salary 藏起来;
最后把回调关掉,权限恢复。
第三段那个「关掉就恢复」值得留意:它正说明「回调在应用进程里」——
所以它挡得住手滑,挡不住有心人。
import sqlite3
import _q46 as Q
c = Q.con()
# 只放行读,写和 DDL 一律拒
def only_select(action, a1, a2, dbname, trigger):
if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ):
return sqlite3.SQLITE_OK
return sqlite3.SQLITE_DENY
c.set_authorizer(only_select)
Q.show(c, "SELECT id, name FROM students")
c.execute("INSERT INTO students (name, height) VALUES ('赵敏', 168.0)")
c.execute("CREATE TABLE tmp (x INT)")
c.execute("DROP TABLE students")
# 表级白名单:students 放行,salary 拒
def no_salary(action, a1, a2, dbname, trigger):
if action in (sqlite3.SQLITE_SELECT, sqlite3.SQLITE_READ):
if a1 == "salary":
return sqlite3.SQLITE_DENY
return sqlite3.SQLITE_OK
c.set_authorizer(no_salary)
Q.show(c, "SELECT id, name FROM students")
c.execute("SELECT id, pay FROM salary")
# 关掉之后就恢复了
c.set_authorizer(None)
Q.show(c, "SELECT COUNT(*) AS 行数 FROM salary")
实跑输出:
--------------------------------------------------------------------
第七十七课:权限管理——grant、revoke,以及这里的替身
--------------------------------------------------------------------
MySQL 里授权与收权
MySQL 原码(本沙箱不执行):
| GRANT SELECT ON shop.orders TO 'report'@'%';
| GRANT SELECT, INSERT ON shop.* TO 'app'@'%';
| GRANT ALL PRIVILEGES ON shop.* TO 'admin'@'%' WITH GRANT OPTION;
| REVOKE INSERT ON shop.* FROM 'app'@'%';
| SHOW GRANTS FOR 'report'@'%';
| FLUSH PRIVILEGES;
**`grant 权限 on 库.表 to 账号`**,**`revoke 权限 on 库.表 from 账号`**。
**权限名是一串关键字**:`select`、`insert`、`update`、`delete`、`create`、`drop`、`alter`、
`index`、`execute`、`all privileges`。
**`on shop.orders` 是表级,`on shop.*` 是库级,`on *.*` 是全局**——**三种范围**。
**`with grant option` 是「允许他再授权给别人」**,**这个要慎给**:
拿到它的人等于可以自己复制权限。
**`flush privileges` 老出现在教程里**,**它的作用是「重新加载权限表到内存」**。
**用 `grant`/`revoke`/`create user` 时数据库会自己刷,不用你管**;
**只有直接 `update mysql.user` 改表的时候才需要——而那种做法本来就不该用。**
**所以这句话多数时候是「抄来的仪式」,不加也行。**
这里没有 grant,但有 set_authorizer
**`set_authorizer` 是 `sqlite3` 的一个回调**:
**每一条语句在执行前,都会先问一句「这个动作允许吗」。**
**你返回「允许」它就做,返回「拒绝」它就报 `not authorized`。**
SQL> SELECT id, name FROM students
id name
------------
1 张伟
2 李娜
3 王强
(3 行)
SQL> INSERT INTO students (name, height) VALUES ('赵敏', 168.0)
报错:not authorized
SQL> CREATE TABLE tmp (x INT)
报错:not authorized
SQL> DROP TABLE students
报错:not authorized
**读得到,写、建表、删表全报 `not authorized`。** **这和 `grant select` 的效果是一样的**——
**只不过一个在数据库里,一个在应用进程里。**
表级白名单:把一张表藏起来
SQL> SELECT id, name FROM students
id name
------------
1 张伟
2 李娜
3 王强
(3 行)
SQL> SELECT id, pay FROM salary
报错:access to salary.id is prohibited
**`students` 能查,`salary` 报 `access to salary.pay is prohibited`。**
**注意报错里带了「哪一列」**——**因为读取的粒度是「表 + 列」。**
**这就相当于「表级权限」**:
**同一张库文件里,有的表能看、有的表不能看。** **这一点比 `mode=ro` 强**——
**只读连接是「全部不能写」,这里是「有的能读、有的不能读」。**
authorizer 的能力边界
**它的判断依据是「动作 + 对象」**:
**动作是 `select`/`insert`/`create` 这些,对象是表名和列名。** **所以能表达的规则有**:
**「这张表不许删」「这一列不许读」「只许建表不许删表」。**
**它做不到的是「按行控制」**:
**它只能拦「读 salary 表」,不能拦「读 salary 表里张三那行」。** **按行的权限,MySQL 也没有**
(那是视图 + `where` 的事,第 11 章)。
**更要紧的一条**:
**`authorizer` 跑在你的进程里,是你的代码在拦你自己。** **所以它不是一个安全边界**——
**能改你代码的人,把回调删掉就绕过去了。**
**真正的安全边界是文件权限和「别让不该连的人连上」。**
**那它有什么用?** **防手滑、防注入。**
**比如一个「只该读」的报表模块,挂上只读 `authorizer` 之后**,
**就算拼接 SQL 被注入了 `drop table`,也执行不了。** **这一层的价值就在这儿。**
最小权限,落到具体做法
**MySQL 上的做法**:**一个应用一个账号**(不要所有人共用一个 `root`)、
**只给需要的库和需要的动作**、**只读模块单独一个账号**、**定期用 `show grants` 复查一遍。**
**本沙箱上的做法**:**只读模块用 `?mode=ro` 打开**、**需要更细的规则就挂 `authorizer`**、
**库文件放在不能被直接下载的位置**、**多租户就一个租户一个文件。**
**两条路的思路是一样的**:
**不是「相信代码不会写错」,而是「写错了也造不成大祸」。** **权限就是那道「兜底」**,
**它平时看不出价值,出事那天能救命。**
第一段:查得到 students,写、建表、删表三条全报 not authorized。
第二段:students 能查,salary 报 access to salary.pay is prohibited。
第三段关掉 authorizer 之后,
salary 又能数出行数了——它的限制是「运行时」的,不是「文件」的。
30 秒实验
- 把回调改成「只拦
DROP」,其他全放行,delete还能做吗? - 为什么
salary那条报错会带列名pay,而不是只说「表不能读」? - 如果这个只读 authorizer 是「安全边界」,那么攻击者要做什么才能绕过它?
常见误区
- 给应用账号
all privileges。 一个where漏写就是全表清空,权限是最后一道闸门。 - 以为
authorizer是安全措施。 它跑在你的进程里,改代码就能绕过,只能防手滑和注入。 - 靠记忆判断权限。 权限会随时间积累,要定期
show grants复查。
自测三问(点开对答案)
1. `grant` 的三种范围分别怎么写? —— `on *.*` 全局、`on 库.*` 库级、`on 库.表` 表级;还有列级 `on 库.表 (列1, 列2)`。 2. `with grant option` 有什么风险? —— 拿到它的人可以把权限再授给别人;你收回来时,他授出去的那些还在。 3. `authorizer` 做不到哪两件事? —— 不能按行控制(只能到表/列),也不是安全边界(跑在自己进程里,改代码就绕过)。第 16 章完了: 安全的三层、账号、权限,都过了。 下一篇进第 17 章——数据备份,这是唯一能在「删错了」之后救你的东西。
- ☐ (本篇完,下一篇 17.1 数据备份简介)