🛢️ 第3章 使用MySQL图形化管理工具
📄 一句话概括

建账号要定三件事:用户名、能从哪台机器连、给它什么权限。 第二件最容易被漏掉,漏了就等于允许任何 IP。 权限不是「有没有」,是「在哪个库、哪张表上、能做什么」。 最小权限不是道德要求,是事故的止损线。

📝 本章和前面几章的关系

上一篇讲编码,这一篇讲「谁能进来、进来能干什么」。 它是第 18 章(用户与权限管理)的预告。 这一篇只讲新建用户这一件事,权限体系的完整讲法在那一章。 和 3.8 节的关系:这一篇建账号,下一篇处理「账号进不去了怎么办」。

一、建账号的三件事

CREATE USER '用户名'@'主机' IDENTIFIED BY '口令'; 这条语句里藏着三件事,第三件(主机)最容易被当成注释。

要素 例子 漏了会怎样
用户名 dev 起名随意,事后不好管
主机段 192.168.1.% 写成 % 等于允许任何 IP 连
口令 Dev@2026 弱口令是被拖库的第一原因

主机段为什么重要?因为 MySQL 的用户是「用户名@主机」这个整体。 [email protected].% 和 dev@% 是两个不同的账号,权限也得分别授。 这就是为什么「我在本机连得上,同事连不上」经常发生: 你建的是 dev@localhost,同事从别的机器来,匹配不上。

二、权限的范围:库、表、列

MySQL 的权限可以授到很细,从全局一直到某一列。

范围 写法 典型场景
全局 ON . 管理员,能管所有库
库级 ON 库名.* 一个项目的应用账号
表级 ON 库名.表名 只允许读某几张表
列级 ON 库名.表名 (列1, 列2) 只让看部分字段,很少用
存储过程级 ON PROCEDURE 名 只允许执行某个过程

日常最常用的是库级和表级。 列级权限很少用,因为它会让 SQL 写法变脆——加一列就可能报没权限。 多层权限是「并集」关系:库级给了 SELECT,表级再给 SELECT 不冲突,也不叠加成两个。 收回的时候要单独 REVOKE,不能靠「再授一次小的」。

三、最小权限:从「够用」开始

教材演示里常给一串权限,实 application 账号最该有的是这四个: SELECT、INSERT、UPDATE、DELETE。

权限 给不给应用账号 理由
SELECT / INSERT / UPDATE / DELETE 给 业务就是这四件事
CREATE / DROP / ALTER 一般不给 改结构该走发布流程,不该由应用随手改
GRANT OPTION 坚决不给 给了它就能把权限再转授给别人
FILE 坚决不给 能读写服务器上的任意文件,等于拿到主机
SUPER / PROCESS 不给 能看所有连接、能杀别人的会话

为什么这么抠?因为出事的往往是应用账号。 应用被注入了、被反编译了,攻击者拿到的就是这个账号的权限。 它有多少权限,损失就有多大。 这不是安全团队的洁癖,是把「一次事故的损失上限」提前定下来。

四、改完权限要刷新,验证要真连一次

GRANT 和 REVOKE 之后要 FLUSH PRIVILEGES;。 8.0 里多数情况会自动生效,但在直接改了权限表之后必须刷。 验证不能只看「语句执行成功」。 要用那个账号真连一次,真跑一条越权的语句,确认它被拒绝。 Demo 里那个校验器就是干这个的:把「谁、从哪来、动哪张表、想干什么」摆出来,逐条判定。 建完用户不验,等于没建。

五、动手 Demo:加一个用户,给一套权限

这一课用一个真的权限校验器把规则演出来。 MySQL 原码照贴,判定逻辑用 Python 复刻——因为沙箱里没有 MySQL 的权限表。 四段:原码、权限模型、七次尝试、几个常被忽略的细节。

SEL, INS, UPD, DEL = 1, 2, 4, 8          # 权限位掩码

USERS = {
    "root@localhost":     ("*",            "*",        SEL|INS|UPD|DEL),
    "[email protected].%":    ("db_database28", "*",        SEL|INS|UPD),
    "report@%":           ("db_database28", "tb_goods", SEL),
}

def match(pat, val):
    if pat in ("*", "%"):
        return True
    if pat.endswith("%") or pat.endswith("."):
        return val.startswith(pat.rstrip("%").rstrip("."))
    return pat == val

def check(user, host, db, table, bit):
    for key, (udb, utb, mask) in USERS.items():
        u, h = key.split("@")
        if u != user or not match(h, host):
            continue
        if not match(udb, db):
            continue
        if utb != "*" and not match(utb, table):
            continue
        return bool(mask & bit)
    return False

for user, host, db, tb, name, bit in ACTIONS:
    print(user, host, tb, name, check(user, host, db, tb, bit))

实跑输出:

--------------------------------------------------------------------
第十五课:加一个用户,给一套权限
--------------------------------------------------------------------

  **3.7 节讲在图形工具里加用户。**
  **加用户是三件事:建账号、定它能从哪台机器连、给它什么权限。**
  **这一课用一个真的权限校验器把这套规则演出来。**
  **MySQL 原码照贴,判定逻辑用 Python 复刻。**

  一、教材原码:建用户与授权
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | CREATE USER 'dev'@'192.168.1.%' IDENTIFIED BY 'Dev@2026';
  | GRANT SELECT, INSERT, UPDATE ON db_database28.* TO 'dev'@'192.168.1.%';
  | GRANT SELECT ON db_database28.tb_goods TO 'dev'@'192.168.1.%';
  | FLUSH PRIVILEGES;
  | REVOKE DELETE ON db_database28.* FROM 'dev'@'192.168.1.%';
  | DROP USER 'dev'@'192.168.1.%';
    (建用户、给库级权限、再叠一层表级权限、刷新生效、收回、删用户)


  二、权限不是「有没有」,是「在哪个范围上有什么」
  --------------------------------------------------------------------

  三、七次尝试,七次判定
  --------------------------------------------------------------------

  谁从哪来         动哪张表                 想干什么  结果
  --------------------------------------------------------
  root@localhost   db_x.tb_y                SELECT    允许
  root@localhost   db_x.tb_y                DELETE    允许
  [email protected]  db_database28.tb_goods   SELECT    允许
  [email protected]  db_database28.tb_goods   DELETE    拒绝
  [email protected]     db_database28.tb_goods   SELECT    拒绝
  [email protected]  db_database28.tb_goods   SELECT    允许
  [email protected]  db_database28.tb_member  SELECT    拒绝
  **最值得看的是第二行和最后一行。**
  **root 可以对任何库任何表删;report 只能查 `tb_goods`,查 `tb_member` 被拒。**
  **这就是「最小权限」:给够用的,一点不多给。**
  **第五行也值得看:dev 从 10.0.0.5 连过来直接被拒。**
  **`'dev'@'192.168.1.%'` 里的主机段不是注释,它是账号的一部分。**

  四、几个常被忽略的细节
  --------------------------------------------------------------------

  细节             不这么做的后果
  ----------------------------------------------------------------
  主机段要写清楚   写成 % 等于允许任何 IP,账号一旦泄露全暴露
  授权后要 FLUSH   改了权限表不刷新,新权限不生效
  表级权限会叠加   库级给了 SELECT,表级再给 SELECT 不冲突,取并集
  回收要用 REVOKE  删用户快,但只想收回一项权限时得 REVOKE
  密码要带复杂度   弱口令是数据库被拖走的第一原因
  **最后一条最实际:教材演示用的是简化口令,真环境里别照抄。**

第一段的四个数字是权限位掩码:1、2、4、8。 用位运算存权限,一条 & 就能判「有没有这一项」,比一串布尔字段省事。 MySQL 内部也是这么干的。 match 那个函数是主机段匹配的核心:% 是通配,192.168.1.% 是前缀。 注意它以 . 结尾也算前缀——192.168.1. 和 192.168.1.% 是一个意思。 第三段七次判定里,第五行最值得看:dev 从 10.0.0.5 连过来被拒。 账号没问题、权限没问题,但主机段对不上。 这就是「我能连、他不能连」最常见的原因。 最后那张表里,GRANT OPTION 和 FILE 两行最该记住。 给了 FILE,等于把服务器上的文件读写权交出去了,这比丢一张表严重得多。

30 秒实验

  1. 把 [email protected].% 改成 dev@%,再跑一次那七行。第五行从「拒绝」变成「允许」。 一行改动,把一个只允许内网的账号变成了允许任何 IP。
  2. 把 report 的权限从 SEL 改成 SEL|INS,看最后一行的判定。 它只能查 tb_goods,查 tb_member 照样被拒——表级权限管得比你想的细。
  3. 想一想:为什么不要给应用账号 DROP 权限?因为一次注入就可能变成一次删表。 应用永远不该有「改结构」的能力,那是发布流程的事。

常见误区

  1. 「主机段写 % 方便,反正有密码。」 那是把「允许任何 IP 尝试登录」写在配置里。 口令一旦泄露,攻击者从哪儿都能进。
  2. 「权限给大了没关系,出事再收。」 出事那一刻已经晚了。 权限决定的是「一次事故的损失上限」,不是「平时方不方便」。
  3. 「GRANT 执行成功,权限就生效了。」 要真连一次、真跑一条越权语句才算验过。 语句成功只说明语法对。
自测三问(点开对答案) 1. `dev@localhost` 和 `dev@%` 是同一个账号吗? —— **不是,MySQL 把「用户名@主机」当整体。** **两个是不同的账号,权限要分别授。** 2. 应用账号最该有的四个权限是什么? —— **SELECT、INSERT、UPDATE、DELETE。** **建表、删表、授权这些不该给应用。** 3. 为什么不能给应用账号 `FILE` 权限? —— **它能读写服务器上的任意文件**,等于越过数据库拿到了主机。**这比丢一张表的损失大得多。**

下一篇讲一件谁都会遇到的事:root 密码忘了。 为什么必须停服务、为什么那几分钟特别危险、以及有没有更稳的办法。

相关笔记