MySQL 5.7+ 配置只读用户需显式授予 SELECT 并回收 INSERT/UPDATE/DELETE/DROP/ALTER 等权限,用反引号包裹库名,禁用 SHOW DATABASES 和限制 INFORMATION_SCHEMA 访问,并检查 host 匹配与全局权限覆盖。

MySQL 5.7+ 如何用 CREATE USER 和 GRANT 配置只读用户
只读权限不是“不给写权限”就完事——漏掉 SELECT 以外的隐式权限,用户仍可能绕过限制。必须显式授予 SELECT,且禁止 INSERT、UPDATE、DELETE、DROP、ALTER 等所有非查询操作。
实操建议:
- 先用
CREATE USER 'ro_user'@'%' IDENTIFIED BY 'strong_pass123';创建用户,避免直接GRANT时自动创建(老版本行为不一致) - 对具体库授权:
GRANT SELECT ON `app_db`.* TO 'ro_user'@'%';—— 注意反引号包裹库名,防止含短横线或关键字的库名出错 - 执行
FLUSH PRIVILEGES;生效(仅在直接改mysql.user表后才强制需要;常规GRANT可省略) - 别忘了
REVOKE INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, INDEX, TRIGGER ON *.* FROM 'ro_user'@'%';—— 尤其当该用户之前被赋予过全局权限时
为什么 SELECT 权限还不够?小心 SHOW 和 INFORMATION_SCHEMA 泄露
默认情况下,MySQL 允许任何已认证用户执行 SHOW DATABASES 和查询 INFORMATION_SCHEMA,这意味着只读用户能看到所有库名、表结构甚至部分元数据(如列注释、索引名)。这不是权限漏洞,而是设计如此。
实操建议:
- 禁用全局库发现:在
my.cnf中添加skip-show-databases(MySQL 5.7+ 支持),重启生效;否则用户执行SHOW DATABASES会返回空列表 - 限制
INFORMATION_SCHEMA访问:MySQL 8.0+ 可用REVOKE SELECT ON INFORMATION_SCHEMA.* FROM 'ro_user'@'%';;5.7 不支持细粒度控制,只能靠应用层过滤或代理层拦截 - 若需隐藏敏感表名,不要依赖权限,而应在业务层用视图封装,只暴露必要字段和行(例如加
WHERE status = 'active')
MySQL 8.0 的角色机制让只读管理更可靠
手写一堆 GRANT 容易漏、难复用。MySQL 8.0 引入角色(ROLE),可把只读权限打包成一个实体,再分配给多个用户——改权限只需改角色,不用逐个用户操作。
实操建议:
- 创建角色:
CREATE ROLE 'readonly_role'; - 赋予权限:
GRANT SELECT ON `reporting_db`.* TO 'readonly_role'; - 绑定用户:
GRANT 'readonly_role' TO 'analyst1'@'%', 'analyst2'@'%'; - 激活角色(对新连接生效):
SET DEFAULT ROLE 'readonly_role' TO 'analyst1'@'%'; - 注意:角色默认不激活,用户登录后需手动
SET ROLE 'readonly_role';,除非设了默认角色
常见错误现象:用户仍能删表 / 修改数据?检查这三处
配置完只读权限却失效,大概率是以下某个地方没堵住:
-
GRANT语句里用了ON *.*而非ON `db_name`.*—— 全局权限会覆盖库级限制 - 用户 host 匹配了多个账户(如
'ro_user'@'localhost'和'ro_user'@'%'),而高优先级的那个有更高权限(MySQL 按 host + user 字典序匹配,localhost优先于%) - 应用连接时用了 root 或其他高权限账号,但代码里又执行了
SET SESSION sql_mode=''类操作,触发了某些存储过程/函数内的权限提升(极少见,但自定义函数若带SQL SECURITY DEFINER且由高权限用户创建,会以定义者权限执行)
验证是否真只读:用该用户登录后执行 SELECT CURRENT_USER(), USER(); 确认实际匹配的账号;再试 INSERT INTO t VALUES (1);,看报错是不是 ERROR 1142 (42000): INSERT command denied to user —— 如果报的是 ERROR 1045 (28000) 或其他,说明根本没连上目标账号。
权限模型本身不复杂,但 MySQL 的 host 匹配逻辑、角色激活时机、以及元数据访问的默认放行,才是日常踩坑最多的地方。


















