数据库安全不是一个开关,是四道门:账号、权限、网络、审计。 少任何一道,剩下三道都白做——尤其「审计」,出事后没它什么都查不到。 最小权限不是「给得少」,是「按角色给」——授到库、表、甚至列。
第 20 章讲权限和安全。
前 19 章一直在「让库干活」,这一章讲「谁能让它干活、能干到什么程度」。
教材 20.1 是「安全保护策略概述」,讲得比较散,这一篇把它整理成四道门。
后面三篇分别讲用户权限(20.2)、安全常见问题(20.3)、日志(20.4)。
沙箱里一条权限语句都不认——CREATE USER、GRANT、REVOKE 全报语法错。
所以这一章走「贴 MySQL 原码 + 用一张 ACL 表复刻语义」的路子。
密码和日志那两块能真跑,而且量出来的数很有意思。
一、四道门:账号、权限、网络、审计
教材 20.1 讲「安全保护策略概述」,内容比较散。整理一下其实就是四道门:
| 门 | 挡什么 | MySQL 里的抓手 |
|---|---|---|
| 账号 | 谁能连上来 | user 表、CREATE USER |
| 权限 | 连上来能干什么 | GRANT/REVOKE |
| 网络 | 从哪儿能连 | bind-address、防火墙 |
| 审计 | 干过什么 | 各种日志 |
四道门是层层递进的:连不上就谈不上权限,有权限也拦不住从外网连,前三道都做到了也还得能查账。 所以「数据库安全」不是一个开关,是四件事拼起来的。少任何一件,剩下的三件都白做。 最容易漏的是第四道。 前三道都是「事前拦」,只有第四道是「事后查」——而事故往往发生在「你以为拦住的地方」。 没有审计,出事了只能猜;有审计,至少能说出「谁在什么时候干了什么」。
二、沙箱里一条权限语句都不认
SQLite 没有用户这个概念,所以教材里这六条语句一条都不通:
| 语句开头 | 沙箱的反应 |
|---|---|
CREATE USER |
near "USER": syntax error |
DROP USER |
near "USER": syntax error |
RENAME USER |
near "RENAME": syntax error |
GRANT |
near "GRANT": syntax error |
REVOKE |
near "REVOKE": syntax error |
FLUSH PRIVILEGES |
near "FLUSH": syntax error |
六条全是 syntax error,但报错位置分两类,这个二分法有用:
CREATE USER 报的是 near "USER"——CREATE 它认,卡在「用户」这个对象类型上。
GRANT 直接报 near "GRANT"——连动词都不认识。
这个差别说明:SQLite 有「建对象」这套语法骨架,但没有「用户」这个对象;
而「授权」是另一种动作,它压根没有。
三、复刻一套 ACL:一张表加一个检查函数
没有权限系统,就用一张表加一个函数复刻。表里三列:谁、对哪张表、能干什么。
GRANT SELECT, INSERT ON shop.tb_g TO 'app'@'localhost' 干的事,就是往这张表插两行。
| 用户 | 表 | 动作 | 结果 |
|---|---|---|---|
| app | tb_g | SELECT | 允许 |
| app | tb_g | DELETE | 拒绝 |
| boss | tb_g | DELETE | 允许 |
| app | tb_user | SELECT | 拒绝 |
第三行:boss 能 DELETE,app 不能——这就是「最小权限」的具体形态。
不是「给得少」,是「按角色给」:谁该干什么就给他什么,多的不给。
第四行是「表级授权」的意思:授权是授到「库.表」上的,不是授到库上的。
所以 app 对 tb_user 什么都没拿到,哪怕它和 tb_g 在同一个库里。
这一点在实际项目里很实用:一个后台系统往往只需要碰三五张表,那就只授那三五张。
出问题时,损失被限制在那几张表里——这是权限系统真正的价值,不是「防外人」,是「限制内伤」。
四、最小权限:一个反面例子
很多教程图省事,直接写 GRANT ALL ON *.* TO 'app'@'%'。这一条同时犯了三个错:
| 犯的错 | 后果 |
|---|---|
ALL |
一个账号被盗,整个库都能删 |
*.* |
能碰 mysql 库本身,等于能改所有账号 |
'%' |
任何 IP 都能连,防火墙形同虚设 |
三个错叠在一起,就是「把 root 的密码写在应用配置里」的等价物。 而它常见的理由只有一个:「先跑通,回头再收紧」——回头通常不会来。 正确的做法是按「这个应用需要碰哪几张表、做哪几个动作」列一遍,只授那几个。 列清单这件事花不了十分钟,但它决定了「出事的时候损失有多大」。 还有一个实践里的细节:读写账号和只读账号要分开。 报表、导出这类需求用只读账号,就算被利用也改不了数据。 这一步的成本几乎为零,收益却是「把一类事故直接变成不可能」。
五、动手 Demo:六条权限语句的报错,再搭一套 ACL
这一段先把六条 MySQL 权限语句挨个试一遍,看报错落在哪个词上;
然后用一张 tb_acl 表加一个 allowed() 函数复刻权限判断,
最后拿一个「给全权限」的反面例子对照最小权限。
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));
CREATE TABLE tb_g (id INTEGER PRIMARY KEY, name TEXT, price REAL);
CREATE TABLE tb_user (name TEXT PRIMARY KEY, real TEXT);
""")
# ① 六条权限语句,一条都不通
for q in (u"CREATE USER 'app'@'localhost' IDENTIFIED BY 'x'",
u"GRANT SELECT ON shop.* TO 'app'@'localhost'",
u"REVOKE SELECT ON shop.* FROM 'app'@'localhost'",
u"FLUSH PRIVILEGES"):
try:
c.execute(q)
except sqlite3.Error as e:
print(q.split()[0], u"->", e)
# ② 用一张表复刻权限
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"), (u"boss", u"tb_g", u"DELETE"),
])
def allowed(user, tbl, act):
return c.execute(u"SELECT 1 FROM tb_acl WHERE user=? AND tbl=? AND act=?",
(user, tbl, act)).fetchone() is not None
for u_, t_, a_ in ((u"app", u"tb_g", u"SELECT"),
(u"app", u"tb_g", u"DELETE"),
(u"boss", u"tb_g", u"DELETE"),
(u"app", u"tb_user", u"SELECT")):
print(u_, t_, a_, u"允许" if allowed(u_, t_, a_) else u"拒绝")
实跑输出:
--------------------------------------------------------------------
安全保护策略:四道门,缺一道都是缺口
--------------------------------------------------------------------
四道门:账号、权限、网络、审计
--------------------------------------------------------------------
**教材 20.1 讲「安全保护策略概述」,讲得比较散。整理一下其实就是四道门:**
门 挡什么 MySQL 里的抓手
----------------------------------------------
账号 谁能连上来 `user` 表、`CREATE USER`
权限 连上来能干什么 `GRANT`/`REVOKE`
网络 从哪儿能连 `bind-address`、防火墙
审计 干过什么 各种日志
**四道门是层层递进的:连不上就谈不上权限,有权限也拦不住从外网连,**
**前三道都做到了也还得能查账。**
**所以「数据库安全」不是一个开关,是四件事拼起来的。少任何一件,剩下的三件都白做。**
沙箱里一条权限语句都不认
--------------------------------------------------------------------
语句开头 沙箱的反应
---------------------------------------
`CREATE` … near "USER": syntax error
`DROP` … near "USER": syntax error
`RENAME` … near "RENAME": syntax error
`GRANT` … near "GRANT": syntax error
`REVOKE` … near "REVOKE": syntax error
`FLUSH` … near "FLUSH": syntax error
**六条全是 `syntax error`,而且报错位置都落在第一个关键字上。**
**`CREATE USER` 报 `near "USER"`(`CREATE` 它认),`GRANT` 直接报 `near "GRANT"`。**
**这个差别说明一件事:SQLite 有「建对象」这套语法(所以 `CREATE` 认),**
**但没有「用户」这个对象类型;而 `GRANT` 是连动词都不认识。**
复刻一套 ACL:一张表加一个检查函数
--------------------------------------------------------------------
**没有权限系统,就用一张表加一个函数复刻。表里三列:谁、对哪张表、能干什么。**
用户 表 动作 结果
---------------------------
app tb_g SELECT 允许
app tb_g DELETE 拒绝
boss tb_g DELETE 允许
app tb_user SELECT 拒绝
**这张表就是 `GRANT` 展开之后的样子。**
**`GRANT SELECT, INSERT ON shop.tb_g TO 'app'@'localhost'` 干的事,就是在权限表里插两行。**
**注意第三行:`boss` 能 `DELETE`,`app` 不能。**
**这就是「最小权限」的具体形态——不是「给得少」,是「按角色给」。**
**第四行是「表级授权」的意思:授权是授到「库.表」上的,不是授到库上的。**
**所以 `app` 对 `tb_user` 什么都没拿到,哪怕它和 `tb_g` 在同一个库里。**
最小权限:一个反面例子
--------------------------------------------------------------------
**很多教程图省事,直接 `GRANT ALL ON *.* TO 'app'@'%'`。这一条同时犯了三个错:**
犯的错 后果
---------------------------------------------
`ALL` 一个账号被盗,整个库都能删
`*.*` 能碰 `mysql` 库本身,等于能改所有账号
`'%'` 任何 IP 都能连,防火墙形同虚设
**三个错叠在一起,就是「把 root 的密码写在应用配置里」的等价物。**
**而它常见的理由只有一个:「先跑通,回头再收紧」——回头通常不会来。**
**正确的做法是按「这个应用需要碰哪几张表、做哪几个动作」列一遍,只授那几个。**
**列清单这件事花不了十分钟,但它决定了「出事的时候损失有多大」。**
第①步四条报错分两类:CREATE 后面卡在 USER,GRANT/REVOKE/FLUSH 卡在第一个词。
前者说明「对象类型不存在」,后者说明「这类动作不存在」——两种缺法的修法不一样。
第②步四行结果里,最值得看的是最后两行:boss 能删、app 不能。
同一张表、同一个动作,差别只在「谁」。这就是权限系统的全部意义。
注意 allowed() 的实现:一次 SELECT 1 ... fetchone() is not None。
没有花哨的东西——权限判断本来就是个查表动作,复杂的是「表怎么维护」。
30 秒实验
- 给
tb_acl加一行('app', 'tb_g', 'UPDATE'),再跑一遍那四条检查,看哪一行变了。 - 把
allowed()改成「如果表名以tb_开头就一律允许」,再跑一遍,体会「通配授权」的风险。 - 加一张
tb_log记每次「拒绝」的动作,想想这算不算最小号的一份审计。
常见误区
- 以为数据库安全就是「设个复杂密码」。密码只是第一道门。 权限、网络、审计缺任何一道,密码再复杂也白搭。
- 以为授权是授到「库」上的。是授到「库.表」上的,还能细到列。 所以「同一个库里就都能碰」是错的。
- 图省事用
GRANT ALL ON *.* TO 'app'@'%'。一个账号泄露就等于整个实例泄露, 包括mysql库本身。
自测三问(点开对答案)
1. 数据库安全的四道门是哪四道? —— 账号(谁能连)、权限(能干什么)、网络(从哪儿连)、审计(干过什么)。 前三道是事前拦,第四道是事后查——少了第四道,出事只能猜。 2. 为什么「最小权限」要授到表级而不是库级? —— 因为一个应用通常只碰几张表。授到库级意味着这个账号能碰库里所有表,包括用户表这类敏感数据。 表级能把损失限制在那几张表里。 3. `CREATE USER` 和 `GRANT` 的报错为什么不一样? —— `CREATE USER` 报 `near "USER"`,说明「建对象」这套语法有,缺的是「用户」这个类型; `GRANT` 报 `near "GRANT"`,说明这类动作压根不存在。两种缺法的含义不同。概述讲完了。下一篇具体到语句:怎么建用户、怎么授权、怎么收权。
- ☐ (本篇完,下一篇 20.2 用户和权限管理)