不能,仅GRANT SELECT不等于真正只读;必须先SHOW GRANTS确认权限,再REVOKE所有写权限(如INSERT、UPDATE、DROP等)及SUPER、GRANT OPTION,限定库级SELECT范围与IP段,并用新连接验证SELECT成功而INSERT报错。

只靠 GRANT SELECT 不等于真正只读
MySQL 权限是叠加的,不是覆盖的。如果用户之前被授予过 ALL PRIVILEGES 或其他写权限(比如通过角色继承、旧账号复用),仅执行 GRANT SELECT ON `db_name`.* 完全无效——残留的 INSERT、UPDATE、DROP 等权限依然生效。
必须先查清当前权限,再显式清理:
- 运行
SHOW GRANTS FOR 'username'@'host';,检查输出是否含INSERT、UPDATE、ALTER、LOCK TABLES、EXECUTE、GRANT OPTION,甚至SUPER -
USAGE权限不表示“只读”,它实际等价于“无任何权限” - 若看到
ALL PRIVILEGES,必须用REVOKE显式收回,不能指望新GRANT覆盖
分步执行:创建 → 授权 → 撤权 → 刷新
以数据库 reporting_db 和用户 bi_reader 为例(MySQL 8.0+):
- 创建用户(限制 IP 更安全):
CREATE USER 'bi_reader'@'10.20.%' IDENTIFIED BY 'StrongPass2026!'; - 授予库级只读:
GRANT SELECT ON `reporting_db`.* TO 'bi_reader'@'10.20.%'; - 显式撤销写类权限(关键!):
REVOKE INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, INDEX, LOCK TABLES, EXECUTE, TRUNCATE, CREATE TEMPORARY TABLES ON `reporting_db`.* FROM 'bi_reader'@'10.20.%'; - 检查并撤销全局高危权限:
SELECT User, Host, Super_priv FROM mysql.user WHERE User = 'bi_reader';,若结果为Y,执行REVOKE SUPER ON *.* FROM 'bi_reader'@'10.20.%'; - 刷新权限:
FLUSH PRIVILEGES;(MySQL 8.0 多数情况自动生效,但建议执行)
验证时必须用新连接,且注意隐式写行为
旧连接会缓存权限快照,复用它测试毫无意义。必须新开终端或新客户端登录后验证:
-
SELECT COUNT(*) FROM reporting_db.users;→ 应成功 -
INSERT INTO reporting_db.users(name) VALUES('test');→ 必须报ERROR 1142 (42000): INSERT command denied -
SELECT * FROM mysql.user;→ 应报错(未授*.*,且mysql库默认不开放) -
SELECT ... FOR UPDATE;→ 必然失败,这不是配置错误,而是你已REVOKE LOCK TABLES,该语句需要锁权限 - 如需
SHOW CREATE TABLE,得额外加GRANT SELECT ON `information_schema`.`TABLES`;否则会因缺失元数据查询权限而失败
MySQL 8.0+ 的角色与动态权限影响
若使用角色管理权限,仅 GRANT SELECT 给用户还不够——角色权限不会自动激活:
- 确认角色已分配:
GRANT 'readonly_role' TO 'bi_reader'@'10.20.%'; - 必须显式设为默认角色:
SET DEFAULT ROLE 'readonly_role' TO 'bi_reader'@'10.20.%'; - 动态权限(如
BINLOG_ADMIN)不参与传统GRANT语法,需单独检查:SELECT * FROM INFORMATION_SCHEMA.APPLICABLE_ROLES; - 避免混淆
read_only系统变量:它是实例级只读开关,需SUPER权限设置,和用户级权限无关
真正难缠的不是授权本身,而是权限残留、角色未激活、以及旧连接缓存——这些点漏掉一个,只读就形同虚设。


















