MySQL的权限体系,本质上是一套白名单机制。创建只读账号的关键在于:不是限制用户不能写什么,而是明确允许他能读什么。一旦理解这个原则,许多问题便迎刃而解。

先说结论:`GRANT SELECT ON *.*`这种写法,表面上授予了整个实例的读权限,实际上却隐藏了风险——它会暴露`mysql`、`performance_schema`等系统库的结构。真正的只读权限必须精确到库表级别,即`GRANT SELECT ON mydb.*`。这是一个不能忽视的细节。
指定库表范围授予SELECT权限,不能仅靠“未授予写权限”来确保安全
以下几个看似简单却容易出错的陷阱值得警惕:
- 库名若包含特殊字符,例如`my-db`,使用单引号包裹写为`'my-db'.*`会直接导致语法错误。正确做法是使用反引号:`my-db`.*,或者直接使用不含特殊字符的库名。
- 跨库JOIN查询是一个隐形陷阱。例如`SELECT * FROM db1.t1 JOIN db2.t2`,如果只授予了`db1`的读权限,而`db2`未授权,查询会报错。两个库都需要显式执行`GRANT SELECT`。
- 许多人认为`REVOKE INSERT`可以加固只读权限,但这其实是多余操作——未授予的权限本来就不存在,回收操作反而容易遗漏`TRUNCATE`、`LOCK TABLES`等隐式写操作。权限管理采用“做加法”比“做减法”更稳妥。
必须单独授予USAGE权限,否则用户无法连接
`GRANT SELECT`只控制“能做什么”,不控制“能否登录”。这就像给了你钥匙,却不告诉你门在哪里。
如果没有`USAGE`权限,密码验证通过后立即断开,报错`ERROR 1045 (28000): Access denied`。在MySQL 8.0及以上版本中,`USAGE`不会被自动附带,因此这一步绝对不能省略。
- 补授权语句:`GRANT USAGE ON *.* TO 'ro_user'@'192.168.10.%'`
- `USAGE`是纯连接权限,不具备任何实际操作能力,必须与`SELECT`分开授予
- 主机名不要使用`'%'`放行所有IP,生产环境建议限定内网段,例如`'192.168.10.%'`
SHOW CREATE TABLE失败?需显式授权INFORMATION_SCHEMA
许多ORM框架、监控工具或DBA在查看表结构时会执行`SHOW CREATE TABLE t1`。该语句底层依赖`information_schema.TABLES`和`information_schema.COLUMNS`的`SELECT`权限。MySQL 5.7默认允许,但8.0及以上版本已彻底收紧。
如果只授予了业务库权限,执行`SHOW CREATE TABLE`就会报错:`ERROR 1142 (42000): SELECT command denied`。
- 解决方法很简单:`GRANT SELECT ON INFORMATION_SCHEMA.* TO 'ro_user'@'192.168.10.%'`
- 不需要全量开放`performance_schema`或`sys`,除非明确需要其视图。
CREATE USER和GRANT必须分两步执行,不能合并为一条语句
从MySQL 5.7开始,`CREATE USER`不再支持同时附带`GRANT`。强行写在一起会直接报错:`ERROR 1064 (42000): You have an error in your SQL syntax`。
- 正确做法分两步:先执行`CREATE USER 'ro_user'@'192.168.10.%' IDENTIFIED BY 'strong-pass-2024';`,再执行`GRANT SELECT ON myapp_db.* TO 'ro_user'@'192.168.10.%';`
- 密码必须用单引号包裹,且不能是纯数字或简单字符串。MySQL 8.0及以上版本默认校验密码强度,太弱会被拒绝创建。
- 执行完`GRANT`后,无需手动执行`FLUSH PRIVILEGES`——因为`GRANT`语句会自动刷新权限缓存,除非直接修改过`mysql.user`表。
最后,还有一个容易被忽略的点:只读账号能执行`SELECT ... FOR UPDATE`吗?答案是**不能**——它触发锁机制,相当于需要`SELECT` + `LOCK TABLES`权限,而只读账号没有后者,直接报错`ERROR 1142`。但值得警惕的是:如果用户拥有`EXECUTE`权限,或能访问`SQL SECURITY DEFINER`视图,仍可能间接触发写操作。权限的边界,远比想象中复杂。