数据库的安全分三层:谁能连上来(账号、密码、主机名)、
连上来能干什么(库级/表级/列级权限)、数据本身怎么保护(加密、脱敏、审计)。
MySQL 的账号是「用户名 + 主机名」一对:
'zhang'@'localhost' 和 'zhang'@'%' 是两个账号。
本沙箱没有账号体系
(create user/grant/revoke/show/drop user 全报 syntax error),
权限退化成三样:文件系统权限、只读连接、语句级白名单。
前面十五章都在讲「怎么把数据管好」,这一章讲「怎么不让不该动的人动」。
这一章在本沙箱里会有点「空」——因为 sqlite3 压根没有账号这个概念。
但这个「空」本身很有信息量:
它说明「数据库权限」不是数据库的固有属性,是服务端模型带来的。
一、MySQL 的安全模型:三层
第一层是「谁能连上来」:账号、密码、以及「从哪台机器连」。
这一层由 mysql.user 那张表管。
第二层是「连上来能干什么」:
库级、表级、列级、甚至「能不能执行某个存储过程」。 这些权限存在 mysql 库的几张表里:
db(库级)、tables_priv(表级)、columns_priv(列级)。
第三层是「数据本身」:加密、脱敏、审计日志。 这一层 MySQL 只提供一部分,
多数要靠应用和外部工具补。
比如「手机号只显示后四位」这件事,数据库管不了,得在查询或渲染时做。
二、账号是「用户名 + 主机名」一对
这是 MySQL 里最容易踩的一个坑:
'zhang'@'localhost' 和 'zhang'@'%' 是两个不同的账号。 建了后者,前者还是不存在。
于是会出现这些怪现象:本地能连、远程连不上(或反过来)、明明给了权限却说没权限、
改了一个账号的密码另一个还能用旧密码进。 十有八九是 host 那一列对不上。
三个常见写法还不完全等价:
localhost 走本机 socket、127.0.0.1 走本机 TCP、% 通配任何主机。
所以在 Linux 上 'u'@'localhost' 和 'u'@'127.0.0.1' 是两个账号,这一条坑过很多人。
三、本沙箱没有账号体系
把教材那五条语句逐条试一遍:create user、grant、revoke、show grants、drop user,
五条全是 syntax error,报错都指向第一个动词:USER、GRANT、REVOKE、SHOW、USER。
原因是结构性的:
sqlite3 不是一个「服务端」,它是「一个文件格式 + 一个库」。
你的程序直接把库文件打开,中间没有「连上来」这个动作,所以也就没有「谁能连」这个问题。
所以「权限」在这里退化成了三样:文件系统权限(谁能读写那个 .db 文件)、
只读连接(打开时声明「只查不改」)、语句级白名单(set_authorizer 拦语句)。
后两样是 16.2 和 16.3 的内容,都能真跑。
四、一个库文件就是一个数据库
这里没有「实例」、没有「服务」、也没有「连接」——
sqlite3.connect('a.db') 打开的是一个文件。 文件不存在就建一个,存在就直接用。
这件事有两面:好处是部署极简(拷一个文件就是备份,删一个文件就是删库);
坏处是没有隔离——同一个文件,任何能读到它的程序都能读到全部数据。
MySQL 那种「这个人只能看这张表」的能力,这里没有。
所以用它做多用户系统时,常见做法是「一个用户一个库文件」,
靠「谁能打开哪个文件」来隔离。 也就是说:隔离的边界是文件系统,不是数据库。
五、最小权限:这一条跟操作系统无关
「给多大权限」这个问题,正确答案永远是「刚好够用的那个」:
一个只读报表账号,就给 select,不要图省事给 all。
理由很实在:
程序有 bug 的时候,权限就是最后一道闸门。
一个只有 select 的账号,delete 写错了也删不掉数据;
一个 all 的账号,一个 where 漏写就是全表清空。
本沙箱里这条原则一样适用:只查的模块用只读连接打开库,别图省事全用读写连接。
这一句声明能挡住一整套误写,代价只是一行 URL 参数。
六、动手 Demo:把五条账号语句逐条试一遍
Demo 就一段:一个循环把 create user、grant、revoke、show grants、drop user 送进去,
看哪条能成。
五条全失败——
这不是「语法写错了」,而是这五个动词在这里不存在。
后面两篇的「替身」都是从这个结论出发的。
import _q46 as Q
c = Q.blank()
# MySQL 的账号与授权语句,逐条试一遍
for s in ("CREATE USER 'zhang'@'%' IDENTIFIED BY 'pwd'",
"GRANT SELECT ON mydb.* TO 'zhang'@'%'",
"REVOKE SELECT ON mydb.* FROM 'zhang'@'%'",
"SHOW GRANTS FOR 'zhang'@'%'",
"DROP USER 'zhang'@'%'"):
try:
c.execute(s)
print("可以:", s)
except Exception as e:
print("不行:", s.split()[0], "->", e)
# 一个连接打开的就是一个文件,没有「连上来」这个动作
print(type(c))
实跑输出:
--------------------------------------------------------------------
第七十五课:安全管理——账号、权限、以及「谁能连上来」
--------------------------------------------------------------------
MySQL 的安全模型:三层
**第一层是「谁能连上来」**:**账号、密码、以及「从哪台机器连」**。
**MySQL 的账号是「用户名 + 主机名」一对**,
**`'zhang'@'localhost'` 和 `'zhang'@'%'` 是两个不同的账号。**
**第二层是「连上来能干什么」**:
**库级权限、表级权限、列级权限、甚至「能不能执行某个存储过程」。**
**这些权限存在 `mysql` 库里的几张表**:
**`user`(账号与全局权限)、`db`(库级)、`tables_priv`(表级)、`columns_priv`(列级)。**
**第三层是「数据本身」**:**加密、脱敏、审计日志。** **这一层 MySQL 只提供一部分**,
**多数要靠应用和外部工具。**
本沙箱没有账号体系
SQL> CREATE USER 'zhang'@'%' IDENTIFIED BY 'pwd'
报错:near "USER": syntax error
SQL> GRANT SELECT ON mydb.* TO 'zhang'@'%'
报错:near "GRANT": syntax error
SQL> REVOKE SELECT ON mydb.* FROM 'zhang'@'%'
报错:near "REVOKE": syntax error
SQL> SHOW GRANTS FOR 'zhang'@'%'
报错:near "SHOW": syntax error
SQL> DROP USER 'zhang'@'%'
报错:near "USER": syntax error
**五条全是 `syntax error`**,报错都指向第一个动词:`USER`、`GRANT`、`REVOKE`、`SHOW`、`USER`。
**这五个词这里一个都不认。**
**原因是结构性的**:**`sqlite3` 不是一个「服务端」,它是一个「文件格式 + 一个库」**。
**你的程序直接把库文件打开,没有「连上来」这个动作,所以也就没有「谁能连」这个问题。**
**所以「权限」在这里退化成了三样东西**:**文件系统权限**(谁能读写那个 `.db` 文件)、
**只读连接**(打开时声明「只查不改」)、**语句级白名单**(`set_authorizer` 拦语句)。
**这三样下面两篇逐个演示。**
一个库文件就是一个数据库
**这里没有「实例」,没有「服务」,也没有「连接」**——
**`sqlite3.connect('a.db')` 打开的是一个文件。** **文件不存在就建一个,存在就直接用。**
**这件事有两面**:**好处是部署极简**(拷一个文件就是备份,删一个文件就是删库);
**坏处是没有「隔离」**——
**同一个文件,任何能读到它的程序都能读到全部数据。**
**MySQL 那种「这个人只能看这张表」的能力,这里没有。**
**所以用它做多用户系统时,常见的做法是「一个用户一个库文件」**:
**A 的数据在 `a.db`、B 的数据在 `b.db`**,
**靠「谁能打开哪个文件」来隔离。** **这也意味着隔离的边界是文件系统,不是数据库。**
MySQL 那边的账号是怎么存的
MySQL 原码(本沙箱不执行):
| SELECT user, host, authentication_string FROM mysql.user;
|
| -- 8.0 之前密码列叫 password,且是 mysql_native_password 的哈希
| -- 8.0 默认改成 caching_sha2_password
**`mysql.user` 这张表就是账号清单**,**`user` + `host` 两列合起来才是「账号」**。
**所以你 `create user 'zhang'@'%'` 之后,`'zhang'@'localhost'` 还是不存在**——
**从本机连也连不上。**
**这个设计经常让人踩坑**:**本地能连、远程连不上**(或者反过来),
**十有八九是 `host` 那一列对不上。**
**`%` 是通配「任何主机」,`localhost` 是「本机 socket」,`127.0.0.1` 又是「本机 TCP」**——
**三个还不完全等价。**
安全管理最要紧的一条:最小权限
**「给多大权限」这个问题,正确答案永远是「刚好够用的那个」**:
**一个只读报表账号,就给 `select`**,**不要图省事给它 `all`。**
**理由很实在**:
**程序有 bug 的时候,权限就是最后一道闸门。**
**一个只有 `select` 的账号,`delete` 写错了也删不掉数据**;
**一个 `all` 的账号,一个 `where` 漏写就是全表清空。**
**本沙箱里这条原则一样适用**:**只查的模块用只读连接打开库**,
**别图省事全用读写连接**——**下一篇会看到,这一句声明能挡住一整套误写。**
五条全报 syntax error,报错分别指向 USER、GRANT、REVOKE、SHOW、USER。
最后一行打印出连接对象的类型:
<class 'sqlite3.Connection'>——它就是「打开的一个文件」,不是一个会话。
30 秒实验
- 为什么
'u'@'localhost'和'u'@'127.0.0.1'在 Linux 上是两个账号? - 「一个用户一个库文件」这种隔离方式,最大的麻烦在哪?
- 如果只读模块用了读写连接打开了库,一个写错的
delete会怎样?
常见误区
- 给应用配
root账号。 一个where漏写就是全表清空,权限是最后一道闸门。 - 以为建了
'u'@'%'本机就能连。localhost是另一个账号,三个 host 写法不通用。 - 把
.db文件放在网站根目录。 一行GET就把整个库下载走了。
自测三问(点开对答案)
1. MySQL 的安全分哪三层? —— 谁能连上来(账号、密码、主机名)、连上来能干什么(库级/表级/列级权限)、 数据本身怎么保护(加密、脱敏、审计)。 2. 为什么本沙箱没有账号体系? —— sqlite3 不是服务端,它是「文件格式 + 一个库」,程序直接打开文件,没有「连上来」这个动作。 3. 本沙箱里「权限」退化成了哪三样? —— 文件系统权限、只读连接(`?mode=ro`)、语句级白名单(`set_authorizer`)。知道安全分几层了,下一篇具体看「账号」这件事:16.2 用户管理——建账号、改密码、删账号。
- ☐ (本篇完,下一篇 16.2 用户管理)