建账号要定三件事:用户名、能从哪台机器连、给它什么权限。 第二件最容易被漏掉,漏了就等于允许任何 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 秒实验
- 把
[email protected].%改成dev@%,再跑一次那七行。第五行从「拒绝」变成「允许」。 一行改动,把一个只允许内网的账号变成了允许任何 IP。 - 把
report的权限从SEL改成SEL|INS,看最后一行的判定。 它只能查tb_goods,查tb_member照样被拒——表级权限管得比你想的细。 - 想一想:为什么不要给应用账号
DROP权限?因为一次注入就可能变成一次删表。 应用永远不该有「改结构」的能力,那是发布流程的事。
常见误区
- 「主机段写
%方便,反正有密码。」 那是把「允许任何 IP 尝试登录」写在配置里。 口令一旦泄露,攻击者从哪儿都能进。 - 「权限给大了没关系,出事再收。」 出事那一刻已经晚了。 权限决定的是「一次事故的损失上限」,不是「平时方不方便」。
- 「
GRANT执行成功,权限就生效了。」 要真连一次、真跑一条越权语句才算验过。 语句成功只说明语法对。
自测三问(点开对答案)
1. `dev@localhost` 和 `dev@%` 是同一个账号吗? —— **不是,MySQL 把「用户名@主机」当整体。** **两个是不同的账号,权限要分别授。** 2. 应用账号最该有的四个权限是什么? —— **SELECT、INSERT、UPDATE、DELETE。** **建表、删表、授权这些不该给应用。** 3. 为什么不能给应用账号 `FILE` 权限? —— **它能读写服务器上的任意文件**,等于越过数据库拿到了主机。**这比丢一张表的损失大得多。**下一篇讲一件谁都会遇到的事:root 密码忘了。 为什么必须停服务、为什么那几分钟特别危险、以及有没有更稳的办法。
- ☐ (本篇完,下一篇 3.8 重置 MySQL 服务器登录密码)