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

数据库安全不是一个开关,是四道门:账号、权限、网络、审计。 少任何一道,剩下三道都白做——尤其「审计」,出事后没它什么都查不到。 最小权限不是「给得少」,是「按角色给」——授到库、表、甚至列。

📝 本章和前面几章的关系

第 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 秒实验

  1. 给 tb_acl 加一行 ('app', 'tb_g', 'UPDATE'),再跑一遍那四条检查,看哪一行变了。
  2. 把 allowed() 改成「如果表名以 tb_ 开头就一律允许」,再跑一遍,体会「通配授权」的风险。
  3. 加一张 tb_log 记每次「拒绝」的动作,想想这算不算最小号的一份审计。

常见误区

  1. 以为数据库安全就是「设个复杂密码」。密码只是第一道门。 权限、网络、审计缺任何一道,密码再复杂也白搭。
  2. 以为授权是授到「库」上的。是授到「库.表」上的,还能细到列。 所以「同一个库里就都能碰」是错的。
  3. 图省事用 GRANT ALL ON *.* TO 'app'@'%'。一个账号泄露就等于整个实例泄露, 包括 mysql 库本身。
自测三问(点开对答案) 1. 数据库安全的四道门是哪四道? —— 账号(谁能连)、权限(能干什么)、网络(从哪儿连)、审计(干过什么)。 前三道是事前拦,第四道是事后查——少了第四道,出事只能猜。 2. 为什么「最小权限」要授到表级而不是库级? —— 因为一个应用通常只碰几张表。授到库级意味着这个账号能碰库里所有表,包括用户表这类敏感数据。 表级能把损失限制在那几张表里。 3. `CREATE USER` 和 `GRANT` 的报错为什么不一样? —— `CREATE USER` 报 `near "USER"`,说明「建对象」这套语法有,缺的是「用户」这个类型; `GRANT` 报 `near "GRANT"`,说明这类动作压根不存在。两种缺法的含义不同。

概述讲完了。下一篇具体到语句:怎么建用户、怎么授权、怎么收权。

相关笔记