只靠GRANT SELECT无法实现真正只读,必须先SHOW GRANTS检查权限,再REVOKE所有写类权限(如INSERT、UPDATE、DELETE等)及GRANT OPTION、SUPER等高危权限,最后GRANT SELECT并FLUSH PRIVILEGES,验证需用新连接测试SELECT成功而INSERT报错。

只靠 GRANT SELECT 无法真正实现只读
MySQL 权限是叠加的,不是覆盖的。执行 GRANT SELECT ON `mydb`.* TO 'ro_user'@'%'; 只会加权限,不会删掉用户已有的 INSERT、UPDATE 或 ALL PRIVILEGES。如果该用户之前被授予过全局权限或角色,那它依然能写数据。
必须先查清现状:SHOW GRANTS FOR 'ro_user'@'%';。输出里只要出现 INSERT、DROP、GRANT OPTION、SUPER 或 USAGE(注意:USAGE 表示“无权限”,不是只读),就说明当前配置不可信。
必须显式 REVOKE 所有非 SELECT 权限
仅 GRANT SELECT 不够,关键在清理。针对目标库(如 reporting_db)要逐项撤销写类操作:
REVOKE INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, INDEX, LOCK TABLES, EXECUTE, TRUNCATE, CREATE TEMPORARY TABLES ON `reporting_db`.* FROM 'ro_user'@'%';- 若用户有
GRANT OPTION,必须额外执行:REVOKE GRANT OPTION ON `reporting_db`.* FROM 'ro_user'@'%'; - 检查是否拥有全局
SUPER:SELECT Super_priv FROM mysql.user WHERE User = 'ro_user' AND Host = '%';,结果为Y就得补一句:REVOKE SUPER ON *.* FROM 'ro_user'@'%';
漏掉任意一项,都可能成为绕过只读的入口——比如 LOCK TABLES 被保留,SELECT ... FOR UPDATE 就仍能执行(虽不改数据,但已破坏只读语义)。
验证时必须用新连接,且测试边界操作
权限变更对已存在的连接无效,MySQL 会缓存权限快照。验证前务必断开重连,否则看到的仍是旧权限状态。
测试不能只跑 SELECT,还得主动触发写类报错:
-
INSERT INTO reporting_db.users(id) VALUES(1);→ 应报ERROR 1142 (42000): INSERT command denied -
SELECT * FROM mysql.user;→ 应失败(没授系统库权限,这是安全表现) -
SHOW CREATE TABLE reporting_db.users;→ 可能失败,因依赖information_schema;如需支持,单独加:GRANT SELECT ON `information_schema`.`TABLES` TO 'ro_user'@'%'; -
SELECT ... FOR UPDATE;→ 必然失败,这不是配置错误,而是你已REVOKE LOCK TABLES,属预期行为
别忽略 IP 限制和密码策略这些生产细节
只读账号一旦泄露,攻击面仍在。生产环境应默认限制来源 IP:
- 创建时用
CREATE USER 'ro_user'@'10.20.5.%' IDENTIFIED BY 'StrongPass2026!';,而非'%' - 强制密码过期:
ALTER USER 'ro_user'@'10.20.5.%' PASSWORD EXPIRE INTERVAL 90 DAY; - 避免
GRANT SELECT ON *.*—— 这会让用户能查mysql库里的哈希密码字段(authentication_string),属于信息泄露
真正难的不是命令怎么写,而是每次授权后都得反向想:用户还能通过什么路径写?有没有角色继承?有没有旧连接没重连?有没有系统库权限被意外打开?这些点一漏,只读就形同虚设。


















