只读账号必须用grant select显式授权,禁用revoke方式;需限定库表范围、包裹特殊库名、分步创建用户并指定mysql_native_password插件;额外授予information_schema.select权限;验证时须测试select for update报错及show grants纯净性。

只读账号必须用 GRANT SELECT,不是禁用写权限
MySQL 没有“只读用户”这种内置类型,所谓只读,就是只给 SELECT 权限。靠 REVOKE INSERT, UPDATE 等方式清理权限是危险的:容易漏掉 TRUNCATE、LOCK TABLES 或 EXECUTE(如果存在函数),反而留下后门。正确做法是白名单思维——只显式授予 SELECT,其余一律不给。
常见错误现象:SHOW GRANTS FOR 'report_user'@'%'; 输出里出现 INSERT 或 USAGE 以外的任何权限,说明授权不干净。
-
GRANT SELECT必须带明确范围,例如GRANT SELECT ON `report_db`.*;不能写GRANT SELECT ON *.*,否则会意外放开mysql、INFORMATION_SCHEMA等系统库 - 库名含短横或关键字(如
order_db)必须用反引号包裹,否则语法报错 - MySQL 8.0 不支持在
CREATE USER语句里直接GRANT,必须拆成两步:先CREATE USER,再GRANT
CREATE USER 要指定认证插件,否则老报表工具连不上
MySQL 8.0 默认用 caching_sha2_password 插件,但很多报表工具(比如旧版 Tableau、某些 Python 2.7 的 mysqlclient)不兼容,会报错:authentication plugin 'caching_sha2_password' cannot be loaded。这不是权限问题,是连接握手失败。
实操建议:
- 查当前插件:
SELECT user, host, plugin FROM mysql.user WHERE user = 'report_user'; - 建用户时显式指定:
CREATE USER 'report_user'@'192.168.20.%' IDENTIFIED WITH mysql_native_password BY 'R3p0rt!2024'; - 主机段用
192.168.20.%限定内网报表服务器 IP 段,别用%—— 防止被扫描利用
报表用户需要 INFORMATION_SCHEMA 才能查表结构
报表系统常执行 SHOW CREATE TABLE orders 或自动探测字段类型,这依赖 INFORMATION_SCHEMA。但默认只授 SELECT ON report_db.* 时,用户访问 INFORMATION_SCHEMA 会失败,报错:ERROR 1142 (42000): SELECT command denied。
解决方案是额外授权:
GRANT SELECT ON INFORMATION_SCHEMA.* TO 'report_user'@'192.168.20.%';- 不需要给
performance_schema或sys,报表基本用不到 - 注意:即使授了
INFORMATION_SCHEMA,普通用户也查不到mysql.user这类敏感表,MySQL 内部做了行级过滤,无需担心
验证时重点测 SELECT ... FOR UPDATE 和 SHOW GRANTS
只读账号上线前最容易忽略的是隐式写操作。报表 SQL 里若混入 SELECT ... FOR UPDATE 或 LOCK IN SHARE MODE,哪怕只有 SELECT 权限也会报错:ERROR 1142 (42000): SELECT command denied —— 因为 MySQL 把锁操作归为写行为,需额外 LOCK TABLES 权限。
验证步骤必须包含:
- 用新账号登录,执行
SELECT COUNT(*) FROM report_db.sales;→ 应成功 - 执行
INSERT INTO report_db.sales VALUES (...);→ 应报INSERT command denied - 执行
SELECT * FROM report_db.sales LIMIT 1 FOR UPDATE;→ 应报错,确认无锁权限 - 执行
SHOW GRANTS FOR CURRENT_USER;→ 输出只能有SELECT和USAGE,绝不能出现其他权限项
最后提醒:MySQL 8.0 中 GRANT 后无需 FLUSH PRIVILEGES,它自动生效;如果改过 mysql.user 表才需要刷。线上环境务必用 CURRENT_USER() 而非 USER() 验证匹配账号,避免因 host 匹配差异导致误判。











