🐬 第28章 安全管理
📄 一句话概括

数据库安全的第一句话是:别所有人都用 root。 MySQL 有四层关卡(能不能连、能看哪个库、 能对哪张表做什么、能做什么动作),SQLite 一层都没有,把安全交给了操作系统。 Demo 用两张表把 MySQL 的授权模型搭出来真跑:alice 只能查 products,查 orders 就被拒; bob 的读权限挂在星号上,哪张表都能读但只能改 orders; 删掉一行授权,同一个人同一张表立刻被拒。

📝 本章和前面几章的关系

写到这一章, 表的建、删、改,数据的安全(事务)都已经覆盖了。 这一章管的是「谁能动这些数据」。 它和上一章的事务是两条腿:事务防「操作做了一半」,权限防「不该做的人做了」。

一、访问控制:权限被拆成四层

第一层是「能不能连上来」:账号加来源主机。 'ben'@'localhost' 与 'ben'@'%' 是两个不同的账号,可以给不同权限。 第二层是「连上能看哪个库」,第三层是「能对哪张表做什么」, 第四层是「能执行哪类语句」(读、写、建表、删表、管理用户)。 这四层是逐层收紧的:上层不给,下层写了也没用。 排查「为什么它连不上」要从第一层往上查,别一上来就看 GRANT。 SQLite 干脆没有这套东西: 谁能打开这个 .db 文件,谁就是全权限。 它的安全模型是「文件系统权限」,所以本课只能模拟。

二、管理用户:建、授权、收回、删

建账号:CREATE USER ben@localhost IDENTIFIED BY 'p@ssw0rd'; 新账号默认什么权限都没有, 连 SELECT 都不行。这是好事,按需给。 授权: GRANT SELECT ON crashcourse.* TO ben@localhost; 库名.表名 里用 * 表示全部。 多个权限用逗号隔开。 收回:REVOKE SELECT ON crashcourse.* FROM ben@localhost; 语法就是 GRANT 那句加个 FROM。 看它现在到底有什么: SHOW GRANTS FOR ben@localhost; 排查「明明授权了还是不行」时,这一句比翻文档快。 删账号:DROP USER ben@localhost;。 注意改密码在 MySQL 里用 ALTER USER ... IDENTIFIED BY ...,GRANT 那条老写法已废弃。

三、本沙箱怎么真跑:把授权当成数据

两张表就够了:users2 存人,grants2 存「谁、什么权限、哪张表」。 这样一建,权限就从「系统配置」变成了「数据」。 判定逻辑只有一句:在这张表里找得到 (用户, 权限, 表或星号) 就允许, 找不到就拒。 真实数据库的权限检查,本质也是查这么一张表。 这层抽象带来一个很有用的认识: 权限是数据,所以改一行就生效,不需要重启服务。 Demo 第四段删掉一行授权,alice 立刻就不能查 products 了。

四、三件必须守的规矩

一,最小权限。 应用账号只给它要用的那几张表、那几个动作。 一个 SQL 注入漏洞能删掉整个库,和只能读一张表,后果差着量级。 二,密码不进代码。 写死在源码里、提交进仓库,等于公开。 放环境变量或密钥管理服务, 而且别用同一个密码喂所有环境。 三,连接要加密。 跨网络的数据库连接走 TLS, 否则密码和查询内容在网络上就是明文。 云数据库现在默认强制要求。

五、小结

教材小结:访问控制分「能连、能看库、能看表、能做什么」四层; 用 CREATE USER 建账号、GRANT 授权、REVOKE 收回、DROP USER 删除。 本课补三条:一是权限本质是一张表,改一行就生效;二是 SQLite 没有账号系统,安全靠文件权限; 三是最小权限加密码外置加加密连接,这三条不做到,其它都是空谈。

六、动手 Demo:用两张表搭一个权限系统

Demo 五段:建两张表模拟 MySQL 的账号与授权,插两个人三条授权,逐条判定谁能干什么, 再删一行授权看它立刻失效,最后讲三条规矩。 注意第四段前后 alice 的判定结果反过来了——中间只做了一件事:删掉一行数据。

import sqlite3
con = sqlite3.connect(":memory:")

# SQLite 没有账号系统,用两张表把 MySQL 的授权模型搭出来
con.execute("CREATE TABLE users2 (name TEXT PRIMARY KEY, role TEXT)")
con.execute("CREATE TABLE grants2 (uname TEXT, priv TEXT, tbl TEXT)")
con.execute("INSERT INTO users2 VALUES ('alice', 'analyst'), ('bob', 'ops')")
con.execute("INSERT INTO grants2 VALUES ('alice', 'SELECT', 'products'), "
            "('bob', 'SELECT', '*'), ('bob', 'UPDATE', 'orders')")

# 判定函数:星号表示「所有表」
def allowed(user, priv, table):
    rows = con.execute("SELECT priv, tbl FROM grants2 WHERE uname = ?",
                       (user,)).fetchall()
    for p, t in rows:
        if p == priv and t in (table, "*"):
            return True
    return False

# 收回 alice 的权限:删一行就生效
con.execute("DELETE FROM grants2 WHERE uname = 'alice' AND priv = 'SELECT'")

实跑输出:

--------------------------------------------------------------------
第二十八课:安全管理
--------------------------------------------------------------------

  【一】MySQL 有四层关卡,SQLite 只有一层
  SQL> CREATE TABLE users2 (name TEXT PRIMARY KEY, role TEXT)
  完成
  SQL> CREATE TABLE grants2 (uname TEXT, priv TEXT, tbl TEXT)
  完成
  **MySQL 的关卡**:能不能连上来(账号加主机)、连上能看哪个库、能对哪张表做什么、
  能做什么操作(SELECT/INSERT/CREATE)。
  **SQLite 没有账号系统**,
  权限就是「这个文件能不能被读」——
  **它把安全交给了操作系统。所以本课用两张表模拟 MySQL 的授权模型。**

  【二】MySQL 的授权语句(教材原码,不执行)
  `CREATE USER ben@localhost IDENTIFIED BY 'p@ssw0rd';`  **建账号**
  `GRANT SELECT ON crashcourse.* TO ben@localhost;`  **只给读**
  `REVOKE SELECT ON crashcourse.* FROM ben@localhost;`  **收回**
  `SHOW GRANTS FOR ben@localhost;`  **看它到底有什么权限**
  `DROP USER ben@localhost;`  **删账号**
  **`'user'@'host'` 这个写法很关键**:同一个用户名从不同机器连进来,
  **可以是两个不同的账号、有不同的权限。**

  【三】本沙箱怎么真跑:把授权模型建出来
  SQL> INSERT INTO users2 VALUES ('alice', 'analyst'), ('bob', 'ops')
  影响 2 行
  SQL> INSERT INTO grants2 VALUES ('alice', 'SELECT', 'products'), ('bob', 'SELECT', '*'),
       ('bob', 'UPDATE', 'orders')
  影响 3 行
  **alice 对 products 做 查询:允许。**
  **alice 对 products 做 插入:被拒(无权限)。**
  **alice 对 orders 做 查询:被拒(无权限)。**
  **bob 对 customers 做 查询:允许。**
  **bob 对 orders 做 更新:允许。**
  SQL> SELECT uname, priv, tbl FROM grants2 ORDER BY uname, priv
  uname     priv       tbl
  ------------------------------
  alice     SELECT     products
  bob       SELECT     *
  bob       UPDATE     orders
  (3 行)
  **alice 只能查 products**,查 orders 就不行;
  **bob 的 SELECT 挂在星号上**,所以哪张表都能查,但只能改 orders。**这就是权限表的样子。**

  【四】收回权限:REVOKE 就是删一行
  SQL> DELETE FROM grants2 WHERE uname = 'alice' AND priv = 'SELECT'
  影响 1 行
  **alice 对 products 做 查询:被拒(无权限)。**
  **同一个人、同一张表,刚才还允许,现在被拒。** **权限就是数据,改一行就生效——不需要重启服务。**

  【五】三件必须守的规矩
  **一,最小权限。** 应用账号只给它要用的那几张表、那几个动作,
  **不要图省事直接给 root。一个注入漏洞就能把库删干净,区别就在这。**
  **二,密码不进代码。** 写在配置文件里还不够,**要放环境变量或密钥管理服务**,
  **更不能提交进代码仓库。**
  **三,连接要加密。** 跨网络的数据库连接必须走 TLS,**否则密码在网络上就是明文。**

第一段建表。 users2 两个人,grants2 三条授权。alice 只有一条,bob 有两条。 第二段逐条判定。 alice 查 products 允许,插 products 被拒,查 orders 也被拒; bob 查 customers 允许(星号),改 orders 也允许。 第三段把授权表打出来。 三行,一眼能看出 alice 为什么只能查一张表——她只有一条授权。 第四段收回权限。 删掉 alice 那条 SELECT,同一句判定从「允许」变成「被拒」。 第五段三条规矩。 最小权限、密码外置、加密连接。

30 秒实验

  1. 给 alice 再加一条 ('alice', 'SELECT', 'orders'),她能查几张表?
  2. 把 bob 那条 ('bob', 'SELECT', '*') 改成 ('bob', 'SELECT', 'products'), 他查 customers 还行吗?
  3. (先想再跑)只删 grants2 里 alice 那一行,users2 里的 alice 还在吗?

常见误区

  1. 应用直接连 root。 一个注入漏洞就能删库,最小权限是唯一便宜的防线。
  2. 密码写在源码或配置里还提交进仓库。 一旦泄露,改密码比改代码快得多,所以密码必须能外部替换。
  3. 以为 SQLite 有账号系统。 它没有,谁拿到文件谁全权限, 对外服务时绝不能把 .db 文件放进可下载目录。
自测三问(点开对答案) 1. MySQL 的访问控制分哪几层? —— 能不能连上来(账号加主机)、能访问哪个库、能对哪张表做什么、能执行哪类语句。 2. GRANT 和 REVOKE 分别做什么? —— GRANT 授予权限,REVOKE 收回权限,两者都按「权限 ON 库.表 TO/FROM 账号」写。 3. 为什么说「权限就是数据」? —— 因为权限记在系统表里,改一行就生效,不需要重启服务;授权检查本质就是查这张表。

权限管住了「谁能动」, 但数据本身会坏、会丢、会变大。 下一章讲维护:备份、体检、日志——出事那天能救命的三样东西。

相关笔记