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

密码存在数据库里,而进数据库又要密码——这是个死循环。 破循环只有一个办法:让服务端「这一趟先不检查密码」。 --skip-grant-tables 好懂但危险,--init-file 麻烦一点但没有那段裸奔窗口。

📝 本章和前面几章的关系

上一篇建账号,这一篇处理「账号进不去了」。 这一篇是全章最「救命」的一节,也是最容易被当成「随便试试」的一节。 教材给的流程是对的,但少了「为什么」和「危险在哪」。 和第 18 章(权限)的关系:重置密码本质是在绕过权限表,所以必须先懂权限表。

一、死循环在哪

root 的口令存在 mysql.user 表里,而读这张表要先以 root 登录。 所以「忘了密码」这件事,用正常途径无解。 还要先接受一件事:库里存的不是密码,是散列。 同一个口令加同一个盐,算出来一样;改一个字符,整串全变。 所以「把密码找回来」在技术上不成立,只能「换一个新的」。

你以为的 实际的
密码存在库里,能查出来 存的是散列,查出来也没用
客服能帮我找回 没人能,包括 DBA
改了文件里一行就行 得让服务端「这一趟先不查口令」

二、skip-grant-tables:跳过权限表启动

流程是五步:停服务、跳过权限表启动、无密码登录、改口令、正常启动。

步 命令 这一步在干什么
1 net stop MySQL80 先停掉正常服务
2 mysqld --skip-grant-tables --shared-memory 启动时跳过权限检查
3 mysql -u root 不用密码就能进
4 改口令,然后 FLUSH PRIVILEGES 让新口令生效
5 net start MySQL80 回到正常模式

第 2 步是全篇最危险的一步:那期间任何人连上来都是超级用户,不需要口令。 所以正确做法里必须加一条:这期间断网,只允许本机连。 第 4 步那个 FLUSH PRIVILEGES 不能省。 跳过权限表启动时,服务端根本没加载权限表,不刷新就改不动。

三、init-file:更稳的那条路

--init-file 指定一个 SQL 文件,服务启动时执行一次,执行完就结束。 它不关鉴权,只是抢在别人连进来之前把口令改掉。

对比 skip-grant-tables --init-file
期间有没有裸奔窗口 有,几分钟 没有
要不要断网 必须 不必
麻烦程度 低 要多写一个文件
能不能远程做 不建议 也不建议,但风险小

所以真出事的时候,专业做法是第二种。 教材写第一种,是因为它更好懂,不是因为它更好。

四、连续输错会怎样:锁

MySQL 8.0 有 FAILED_LOGIN_ATTEMPTS 和 PASSWORD_LOCK_TIME 两个参数。 连错几次就锁一段时间,锁定期内连对的密码也进不去。 这是故意的:否则攻击者可以边锁边试,锁定就没意义了。 它带来一个很实际的后果。 线上真出事的时候,「重置密码」往往不是第一步,「等锁定过期」才是。 很多人不知道这一点,急着重置,反而把服务多重启了一次。 防御这件事的正解不是重置流程,是别走到这一步: 口令存进密码管理器、建一个日常用的管理员账号、别拿 root 干日常活。

五、动手 Demo:忘了 root 密码怎么办

这一课把教材那套流程贴出来,再算清楚两件事:密码是怎么存的、锁是怎么生效的。 五段:原码、散列、危险在哪、锁定模拟、三种找回方式对比。

import hashlib

def make(pwd, salt):
    return hashlib.sha256((salt + pwd).encode("utf-8")).hexdigest()

salt = "a1b2c3"
for pwd in ("123456", "123456", "1234567"):
    print(pwd, make(pwd, salt)[:24] + "...")

LOCK_AFTER, LOCK_SEC = 5, 60
state = {"fail": 0, "until": 0}

def login(pwd, now):
    if now < state["until"]:
        return "拒绝:还在锁定期"
    if make(pwd, salt) == make("NewPwd@2026", salt):
        state["fail"] = 0
        return "成功"
    state["fail"] += 1
    if state["fail"] >= LOCK_AFTER:
        state["until"] = now + LOCK_SEC
        state["fail"] = 0
        return "失败:连错 %d 次,锁 %d 秒" % (LOCK_AFTER, LOCK_SEC)
    return "失败:还可再试 %d 次" % (LOCK_AFTER - state["fail"])

实跑输出:

--------------------------------------------------------------------
第十六课:忘了 root 密码怎么办
--------------------------------------------------------------------

  **3.8 节讲重置密码。这一节的价值不在「怎么改」,在「为什么这么麻烦」。**
  **密码存在数据库里,而进数据库又要密码——这就是个死循环。**
  **破这个循环的办法只有一个:让服务端「这一趟先不检查密码」。**

  一、教材原码:跳过权限表启动
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | net stop MySQL80
  | mysqld --skip-grant-tables --shared-memory
  | mysql -u root
  | UPDATE mysql.user SET authentication_string = ''
  |   WHERE user = 'root' AND host = 'localhost';
  | FLUSH PRIVILEGES;
  | ALTER USER 'root'@'localhost' IDENTIFIED BY 'NewPwd@2026';
  | net start MySQL80
    (停服务、跳过权限表启动、无密码登录、清空旧口令、刷新、设新口令、正常启动)


  二、密码是怎么存的:不是密码,是散列
  --------------------------------------------------------------------

  口令     存进库里的样子             说明
  -----------------------------------------------
  123456   39c38e45f568ce9a99e26811…  长度固定 64
  123456   39c38e45f568ce9a99e26811…  长度固定 64
  1234567  eda2f9b599f2a2d2fbfbd9e8…  长度固定 64
  **同一个口令、同一个盐,算出来的散列一样;改一个字符,整串全变。**
  **所以「密码丢了找回来」这件事在技术上不成立——库里根本没有原密码。**

  三、为什么 --skip-grant-tables 是危险动作
  --------------------------------------------------------------------
  **它把「检查权限」这一步整个关掉。**
  **这期间任何人连上来都是超级用户,不需要口令。**
  **所以正确流程里有两个必做动作。**
  **一是改完立刻 `FLUSH PRIVILEGES` 恢复检查,二是改完立刻恢复正常启动。**
  **还有一条:这期间最好把网络断掉,只允许本机连。**

  四、连续输错会怎样:锁
  --------------------------------------------------------------------

  第几次    什么时候  结果
  ----------------------------------------------
  错1       第 0 秒   失败:还可再试 4 次
  错2       第 1 秒   失败:还可再试 3 次
  错3       第 2 秒   失败:还可再试 2 次
  错4       第 3 秒   失败:还可再试 1 次
  错5       第 4 秒   失败:连错 5 次,锁 60 秒
  锁定期内  第 10 秒  拒绝:还在锁定期,剩 54 秒
  锁过期后  第 70 秒  成功
  **注意第六行:锁定期内连对的密码也进不去。**
  **这是故意的——否则攻击者可以边锁边试。**
  **也正因为这个,线上真出事的时候,「重置密码」往往不是第一步,「等锁定过期」才是。**

  五、三种找回方式,代价各不同
  --------------------------------------------------------------------

  方式               怎么做                                      代价
  -----------------------------------------------------------------------------------------
  skip-grant-tables  停服务、跳过权限表启动、改口令、再启回来    服务中断几分钟,期间无鉴权
  init-file          写一个只含改口令语句的文件,启动时执行一次  也要重启,但不用关鉴权
  重装               卸掉重装,数据目录保留                      配置全丢,还可能把数据弄坏
  **三种里最稳的是第二种:`--init-file` 指定一个脚本,服务启动时执行完就删掉。**
  **它没有「关鉴权」那段窗口期。**
  **而真正的正解是别走到这一步:把 root 口令存到密码管理器里,再建一个日常用的管理员账号。**

第一段三行说明一件事:口令 1 和 2 一样,散列就一样;第 3 个改了一个字符,整串全变。 所以库里那串东西不能「反算」回原口令。 这张表的长度是固定的 64(十六进制 64 位)——不管口令多长。 这也解释了为什么「密码最长 20 位」这种限制是多余的:存的是散列,长度与口令无关。 第二段那个锁定模拟里,第六行最反直觉:锁定期内连对的密码也进不去。 这是故意的,代价是「真出事了得等」。 所以看到「连对也登不上」,先想是不是被锁了,别急着改密码。 最后那张对比表,只有四行,但第一行是全篇的重点: skip-grant-tables 有一段时间是完全不设防的。 知道这一点,才会理解为什么专业做法选 --init-file。

30 秒实验

  1. 把 Demo 里的 LOCK_AFTER 改成 3,连错三次再试对的密码。你会发现对的密码也被锁在外面。
  2. 把 make 里的 salt 去掉(只对密码本身做散列),想想为什么真实的系统一定要加盐。 不加盐,两个用户用同一个口令就会算出同一个散列,撞库一次就能批量试。
  3. 想一想:为什么 --skip-grant-tables 启动时必须带上 --shared-memory(Windows)? 因为这种模式下默认不监听网络,得靠共享内存让本机客户端连进去。 这其实是一道安全设计——尽量不让远程有机会连。

常见误区

  1. 「用 --skip-grant-tables 起服务,改完密码就行了。」 那几分钟里谁都能连,而且不用密码。 必须断网,改完立刻 FLUSH PRIVILEGES 并恢复正常启动。
  2. 「密码忘了,找 DBA 查一下原密码。」 库里没有原密码,只有散列。 谁都查不出来,只能重置。
  3. 「连对了密码还是进不去,肯定是密码没改对。」 也可能是被锁了。 8.0 有失败次数限制,锁定期内正确的口令同样被拒。
自测三问(点开对答案) 1. 为什么不能「找回」原密码? —— **因为库里存的是散列,不是口令本身。** **散列是单向的,反算不出来。** 2. `--skip-grant-tables` 期间最大的风险是什么? —— **服务端完全不检查权限,任何人连上来都是超级用户。** **所以要断网、改完立刻刷新并正常启动。** 3. 为什么更推荐 `--init-file`? —— **它不关鉴权,只是抢在别人连进来之前把口令改掉,没有那段裸奔窗口。**

第 3 章到这儿结束。下一篇进第 4 章,正式讲 SQL 的库级操作—— 建库、查看、选择、修改、删除,一条条讲语法和参数。

相关笔记