正确创建MySQL只读账户需显式GRANT SELECT ON sales_db.*,禁用其他权限;PostgreSQL还需禁用CREATEDB/CREATEROLE属性,并在目标库内授权;应用层须严格管控配置与连接池语句。
MySQL 创建只读账户的正确 SQL 语句
只读账户不是靠“不给写权限”来实现的,而是明确授予 select 权限,并且**不授予任何其他 dml 或 ddl 权限**。常见错误是只执行 revoke insert, update, delete on *.* from 'user'@'%',这毫无意义——权限默认就是没授予的,撤回一个不存在的权限不会生效,也不会阻止后续误授权。
正确做法是显式 GRANT SELECT,并确保不执行任何其他 GRANT:
CREATE USER 'report_user'@'%' IDENTIFIED BY 'strong_password'; GRANT SELECT ON `sales_db`.* TO 'report_user'@'%'; FLUSH PRIVILEGES;
-
GRANT SELECT ON *.*是危险的,会暴露所有库的表结构(哪怕查不到数据),应限定到具体数据库,如`sales_db`.* - 不要用
GRANT USAGE初始化用户——它不带任何权限,但容易让人误以为“已创建好只读账号”,结果实际连SELECT都没给 -
FLUSH PRIVILEGES在大多数 MySQL 8.0+ 场景下非必需,但加了更稳妥;不加时依赖权限缓存刷新,可能延迟生效
PostgreSQL 中只读角色必须禁用 CREATEDB 和 CREATEROLE
PostgreSQL 的“只读”比 MySQL 更隐蔽:即使只给了 SELECT 权限,如果用户拥有 CREATEDB 或 CREATEROLE 属性,就能自己建库、建用户、再赋权绕过限制。这不是权限粒度问题,而是角色属性层面的越权入口。
创建安全只读角色的最小操作集:
CREATE ROLE report_user WITH LOGIN PASSWORD 'xxx'; GRANT CONNECT ON DATABASE sales_db TO report_user; \c sales_db GRANT USAGE ON SCHEMA public TO report_user; GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_user;
- 必须显式执行
\c sales_db切换数据库,否则GRANT SELECT ON ALL TABLES会报错——PostgreSQL 要求在目标库内授表级权限 - 新表默认不会自动继承只读权限,需补上
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO report_user - 检查用户是否意外有
CREATEDB:运行SELECT rolcreatedb, rolcreaterole FROM pg_roles WHERE rolname = 'report_user';,两个字段都应为f
连接层限制:应用配置里别留 root 密码的影子
数据库账号只是第一道门,真正常被绕过的点在应用配置。比如 Django 的 DATABASES 配置里写的是 'USER': 'root',但注释写着“测试环境用”,上线却忘了改——这种硬编码比权限配置本身更致命。
- 环境变量加载数据库凭据,而非直接写进代码:
os.getenv('DB_USER'),并在部署时严格管控DB_USER值为只读账号名 - Spring Boot 的
application-prod.yml必须和application-dev.yml分离,禁止用@Profile("dev")包裹生产配置 - 连接池(如 HikariCP)配置中检查
connection-test-query是否用了SELECT 1而非INSERT ...类语句——某些旧版驱动或中间件会在初始化时执行该语句,若账号无写权限会导致启动失败
验证只读是否真生效:别只测 SELECT
验证不能停留在“能查出数据”,要主动尝试越权操作并确认报错类型。很多 DBA 只跑一条 SELECT COUNT(*) FROM users 就认为 OK,但漏掉了触发器、临时表、函数调用等隐式写行为。
- 在账号下执行
INSERT INTO t VALUES (1),预期错误是ERROR 1142 (42000): INSERT command denied to user(MySQL)或ERROR: permission denied for table t(PG);若报Access denied或连接中断,说明网络/认证层出了问题,不是权限配置生效 - 测试
CREATE TEMP TABLE tmp AS SELECT * FROM t;—— PostgreSQL 允许只读用户建临时表,MySQL 不允许,需按实际需求决定是否额外禁用(PG 中用REVOKE TEMP ON DATABASE db_name FROM role) - 调用含写操作的存储过程(如 MySQL 的
CALL update_stats())必须失败;若成功,说明过程定义者拥有SQL SECURITY DEFINER且定义者是高权限账号,这是设计缺陷,不是权限配置不到位
最易被忽略的是跨库查询场景:MySQL 中 SELECT * FROM sales_db.users JOIN audit_log_db.events,只要对 audit_log_db 没授 SELECT,整条语句就失败——但开发者常假设“我只查自己库的表”,忘了 JOIN 会触发另一库的权限校验。

















