密码存在数据库里,而进数据库又要密码——这是个死循环。
破循环只有一个办法:让服务端「这一趟先不检查密码」。
--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 秒实验
- 把 Demo 里的
LOCK_AFTER改成 3,连错三次再试对的密码。你会发现对的密码也被锁在外面。 - 把
make里的salt去掉(只对密码本身做散列),想想为什么真实的系统一定要加盐。 不加盐,两个用户用同一个口令就会算出同一个散列,撞库一次就能批量试。 - 想一想:为什么
--skip-grant-tables启动时必须带上--shared-memory(Windows)? 因为这种模式下默认不监听网络,得靠共享内存让本机客户端连进去。 这其实是一道安全设计——尽量不让远程有机会连。
常见误区
- 「用
--skip-grant-tables起服务,改完密码就行了。」 那几分钟里谁都能连,而且不用密码。 必须断网,改完立刻FLUSH PRIVILEGES并恢复正常启动。 - 「密码忘了,找 DBA 查一下原密码。」 库里没有原密码,只有散列。 谁都查不出来,只能重置。
- 「连对了密码还是进不去,肯定是密码没改对。」 也可能是被锁了。 8.0 有失败次数限制,锁定期内正确的口令同样被拒。
自测三问(点开对答案)
1. 为什么不能「找回」原密码? —— **因为库里存的是散列,不是口令本身。** **散列是单向的,反算不出来。** 2. `--skip-grant-tables` 期间最大的风险是什么? —— **服务端完全不检查权限,任何人连上来都是超级用户。** **所以要断网、改完立刻刷新并正常启动。** 3. 为什么更推荐 `--init-file`? —— **它不关鉴权,只是抢在别人连进来之前把口令改掉,没有那段裸奔窗口。**第 3 章到这儿结束。下一篇进第 4 章,正式讲 SQL 的库级操作—— 建库、查看、选择、修改、删除,一条条讲语法和参数。
- ☐ (本篇完,下一篇 4.1 认识数据库)