mysql只读用户需通过显式授予select权限且禁用写权限实现,必须检查并清理用户已有权限、精确限定主机范围、强制密码轮换,并验证关联表及information_schema访问权限。

创建只读用户前必须确认的权限模型
MySQL 的只读权限不是靠一个“READ ONLY”开关实现的,而是通过显式授予 SELECT 权限、且不授予任何写操作权限来达成。关键点在于:即使你没给 INSERT/UPDATE/DELETE,只要用户拥有 GRANT OPTION 或被授予了 USAGE 以外的全局权限(比如 PROCESS、SUPER),就可能间接绕过限制。所以第一步永远是检查当前账户是否干净。
实操建议:
- 先用
SHOW GRANTS FOR 'username'@'host';查看目标用户已有权限,避免叠加冲突 - 如果用户已存在,优先用
DROP USER 'username'@'host';彻底重建,比REVOKE更可靠 - 不要在
mysql系统库上授予权限——哪怕只是SELECT,也可能暴露用户表或密码哈希
用 CREATE USER + GRANT 组合完成最小化授权
最稳妥的方式是分两步:先创建用户并设密码,再单独授予指定数据库的 SELECT 权限。注意不能把 CREATE USER 和 GRANT 合并在一条语句里(MySQL 8.0+ 虽支持,但容易漏掉认证插件或过期策略等细节)。
实操建议:
- 创建用户时明确指定认证方式,例如:
CREATE USER 'reporter'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'strong_pass_2024'; - 只对具体库授权:
GRANT SELECT ON `sales_db`.* TO 'reporter'@'192.168.1.%';(反引号包裹库名,防止特殊字符出错) - 如需跨多个库,逐个执行
GRANT SELECT ON `db1`.*、GRANT SELECT ON `db2`.*,不要用GRANT SELECT ON *.* - 执行
FLUSH PRIVILEGES;—— 这步在大多数情况下非必需,但遇到权限未生效时它是第一排查项
验证只读效果的三个必测动作
光看 SHOW GRANTS 不够,必须实际测试。常见错误是用户能连上、能查表,但一执行 SELECT COUNT(*) FROM t1 JOIN t2 就报错,其实是缺少对关联表的 SELECT 权限。
实操建议:
- 用新用户登录后,立刻运行:
SELECT USER(), CURRENT_USER();确认身份匹配(CURRENT_USER()显示实际匹配的账号) - 尝试写操作:
INSERT INTO t1 VALUES (1);、CREATE TEMPORARY TABLE tmp AS SELECT 1;、SET @a=1;—— 最后一个会成功,因为变量设置不依赖权限,但能帮你确认连接本身没问题 - 如果应用使用 ORM(如 Django、Laravel),注意它们常自动执行
SHOW FULL COLUMNS或SELECT ... FROM information_schema,需额外授予SELECT权限给information_schema库(仅限必要表,如COLUMNS、TABLES)
生产环境必须关闭的两个默认行为
MySQL 默认允许用户从任意主机连接(%),也默认不限制密码过期。这两点对只读用户同样危险:一旦密码泄露,攻击者可从公网直连;长期不更新密码则增加撞库风险。
实操建议:
- 主机白名单要精确到子网,例如用
'reporter'@'10.10.20.%'替代'reporter'@'%';若应用部署在 Kubernetes,考虑用 Service IP 或 Pod CIDR - 强制密码轮换:
ALTER USER 'reporter'@'10.10.20.%' PASSWORD EXPIRE INTERVAL 90 DAY; - 禁用历史密码重用:
ALTER USER 'reporter'@'10.10.20.%' PASSWORD HISTORY 5;
权限粒度越细,后期维护成本越高。比如按业务模块拆成 sales_ro、log_ro,比一个万能 readonly 用户更安全,但也意味着每次新增表都要重复 GRANT。真正难的不是语法,是想清楚「这个用户到底该看到哪几行数据」。











