权限管理的核心目标不是“把门锁上”,而是让每个角色只能做它该做的事。一个合理的权限分配,能在源头上防止误操作、阻止越权访问,甚至在应用层出现 SQL 注入时降低损失。这一节介绍 MySQL 中权限的具体授予与回收方式,以及贯穿始终的最小权限原则。
23.3.1 权限授予:GRANT 语句
MySQL 使用 GRANT 语句为已存在的用户授予权限。基本语法如下:
GRANT 权限列表 ON 权限范围 TO '用户名'@'主机' [WITH 选项];
权限列表可以是 ALL PRIVILEGES(赋予指定级别的全部权限),也可以是一个或多个具体权限,用逗号分隔,比如 SELECT, INSERT, UPDATE。MySQL 的权限种类非常多,从数据操作(SELECT、INSERT、UPDATE、DELETE)到结构管理(CREATE、ALTER、DROP),再到运维管理(SUPER、PROCESS、SHUTDOWN),覆盖了所有操作需求。常见的权限参见 23.2 节的权限表。
权限范围决定了这个权限作用的层级,它有五种典型写法:
.— 全局级别,所有数据库的所有表。db_name.*— 数据库级别,指定库下的所有表。db_name.table_name— 表级别,指定某个表。db_name.table_name.column_name— 列级别,仅作用于指定列(只能是SELECT、INSERT、UPDATE权限)。- 存储过程、函数、代理用户等特定对象也有其对应的范围写法。
'用户名'@'主机' 必须精确匹配已有的用户标识,GRANT 不会自动创建用户(除非显式使用 IDENTIFIED BY 子句,但官方已不推荐)。在授予权限之前,需要先用 CREATE USER 创建用户。
WITH 选项 可选,常见的有:
WITH GRANT OPTION— 让该用户可以将自己拥有的权限再授予其他用户。这是一把“能够传递权限”的钥匙,需要非常谨慎地发放。MAX_QUERIES_PER_HOUR、MAX_UPDATES_PER_HOUR、MAX_CONNECTIONS_PER_HOUR等资源限制选项,用于限制用户每小时执行的查询次数、更新次数或连接次数。
示例:
-- 创建用户
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
-- 授予对 app_db 库的所有权限,并允许将该权限授予他人
GRANT ALL PRIVILEGES ON app_db.* TO 'app_user'@'%' WITH GRANT OPTION;
-- 授予只读权限,仅限所有库所有表(注意:*.* 是全局级,会覆盖所有数据库)
GRANT SELECT ON *.* TO 'readonly_user'@'%';
-- 授予对特定表的列级权限,只允许查看用户名和邮箱
GRANT SELECT (username, email) ON app_db.users TO 'api_user'@'%';
执行 GRANT 后,最好紧接着执行 FLUSH PRIVILEGES; 让权限生效。不过从 MySQL 5.1 起,直接用 GRANT 已经会自动刷新权限,但执行显式刷新是一个好习惯,特别在使用了 INSERT 直接修改系统表(如 mysql.user)的旧式操作时。
23.3.2 权限回收:REVOKE 语句
权限可以给,就可以收。REVOKE 语法与 GRANT 几乎对称,只是将 GRANT 换成 REVOKE,并且 TO 换为 FROM:
REVOKE 权限列表 ON 权限范围 FROM '用户名'@'主机';
示例:
-- 回收 app_user 对 app_db 的 DELETE 权限
REVOKE DELETE ON app_db.* FROM 'app_user'@'%';
-- 回收用户的所有全局权限
REVOKE ALL PRIVILEGES ON *.* FROM 'admin_user'@'localhost';
-- 回收 GRANT OPTION(注意,必须用单独的 REVOKE GRANT OPTION ON ...)
REVOKE GRANT OPTION ON app_db.* FROM 'app_user'@'%';
回收时需要注意几点:
- 一个用户可能通过多个账户定义(不同主机)拥有权限,回收必须精确匹配
'用户名'@'主机'。 - 权限可以分层级,如果你对全局授予了
SELECT,后来又在某些库上执行了REVOKE,则需要仔细验证最终效果;有时残留的全局权限仍然使回收不彻底,建议用SHOW GRANTS FOR '用户名'@'主机'进行确认。 - 如果用户拥有
WITH GRANT OPTION,你既可以回收具体权限,也可以单独回收GRANT OPTION,后者会保留用户自身的权限,仅移除了授权他人能力。
对于已经不再需要某些权限的用户,及时回收是安全维护的关键一步。切忌保留已离职开发者的高权限账户,也不要让测试用户拥有生产环境的写权限。
23.3.3 查看用户权限
授予和回收前后,都需要确认用户当前拥有的确切权限。常用命令:
-- 查看当前登录用户的权限
SHOW GRANTS;
-- 查看指定用户的权限
SHOW GRANTS FOR 'app_user'@'%';
输出类似 GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'%',清晰列出每个级别的权限。这比直接查询 mysql.user、mysql.db 等表更直观。
23.3.4 最小权限原则
最小权限原则(Principle of Least Privilege)是安全领域的黄金法则:每个用户(或服务)仅应被授予完成其任务所必需的最小权限集合。这一点在数据库权限管理中尤其重要,因为数据库通常是攻击者最感兴趣的目标。
在 MySQL 中实践最小权限原则,可以从这几个维度着手:
1. 角色隔离,按需分配
不要给所有开发者或应用账号授予 ALL PRIVILEGES ON .。应根据实际使用场景建立清晰的权限角色模型。例如:
- 应用程序连接账号:通常只需要对业务库进行数据操作(
SELECT, INSERT, UPDATE, DELETE),完全不需要CREATE, DROP, ALTER等结构修改权限,更不需要FILE、SUPER等系统权限。如果不涉及跨库关联查询,甚至只局限于一个库app_db.*,减少影响范围。 - 只读查询账号:用于报表或数据分析类应用,仅授予
SELECT,且只限于特定库或表。如果读取敏感字段(如用户手机号、密码哈希),可进一步细化到列级别。 - 运维管理账号:分配给 DBA 或自动化脚本,拥有所有全局管理权限。但要严格限制登录主机(如仅限
localhost或特定管理网段),并启用强密码和审计。 - 开发测试账号:可以使用独立的测试实例,或在生产库上仅授予读取权限;绝对禁止授予
DROP、ALTER等危险权限。
2. 限定访问来源
'用户名'@'主机' 中的主机部分也是最小权限的一部分。应用程序账号如果只在特定应用服务器上运行,就指定具体的 IP 或 IP 段,比如 'app_user'@'192.168.10.101',而不是 'app_user'@'%'。即使账号密码泄露,攻击者从其他机器也无法连接。
3. 区分读写、结构修改与管理权限
常见误区是“为了方便”,把 ALL PRIVILEGES 一把抛给应用账号。结果一个 SQL 注入就被执行了 DROP TABLE。应该只给应用账号 CRUD 权限,对数据表结构的变动(如添加索引、修改字段)统一通过 DBA 或部署系统进行。这样即使应用层被注入,攻击者也只能在数据层面读写,无法清库。
4. 定期审计与回收
权限不是一劳永逸的。项目初期可能图方便给了很多宽松权限,随着系统稳定,应该逐步回收多余权限。例如,数据迁移完成后,回收 FILE 权限;不再需要持有 SUPER 的账号,及时收回。
建议定期执行以下检查:
- 列出拥有全局权限(
.)的用户,确认是否必要。 - 找出密码过期或长期未活跃的用户并禁用。
- 检查
GRANT OPTION是否被不当分配。
5. 善用 MySQL 8.0 的角色(Role)功能
MySQL 8.0 引入了角色,使得权限管理更加模块化。你可以先创建角色,再把角色授予用户。
CREATE ROLE 'app_read', 'app_write';
GRANT SELECT ON app_db.* TO 'app_read';
GRANT INSERT, UPDATE, DELETE ON app_db.* TO 'app_write';
-- 将只读角色授予只读账号,将读写角色授予业务账号
GRANT 'app_read' TO 'report_user'@'%';
GRANT 'app_read', 'app_write' TO 'app_user'@'%';
这样当权限变更时,只需修改角色,所有拥有该角色的用户自动继承变动,降低管理成本,也更易贯彻最小权限原则。
总结
权限授予与回收操作并不复杂,真正考验的是管理意识:在你给出一个权限之前,先问自己“这个账号真的需要这个权限吗”。养成这样的习惯,比任何复杂的安全插件都更有效。最小权限原则不是一根筋地拒绝所有权限,而是让每个权限都有其明确且必要的理由,这才是生产环境数据库安全的第一道闸门。