ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MySQL安全性控制:权限表、GRANT/REVOKE与最小权限实战

MySQL安全性控制:权限表、GRANT/REVOKE与最小权限实战 1. 为什么MySQL安全性控制值得单独拿出来啃接触过头歌EduCoder平台的朋友应该都有体会它的实验关卡设计得很鸡贼——任务描述只有寥寥几行但判题脚本卡的点特别细。MySQL安全性控制这一关尤其如此很多人第一次做的时候明明 GRANT 语句敲得没问题提交上去就是不通过因为判题脚本校验的是mysql.user表里的最终状态而不是你屏幕上那句Query OK。这一篇我想把 MySQL 的安全性控制体系从头到尾捋一遍不只是告诉你哪条命令能过判题更要讲清楚这套权限模型为什么长这样、每一层权限是怎么被匹配到的、哪些参数是必须精算的、哪些坑是只有踩过才知道的。内容适合三类人正在头歌上刷 MySQL 实验的学生、刚开始接手数据库运维的初级 DBA、以及写业务代码但需要自己开账号授权的后端开发。前者能直接抄作业过实验后两者能拿到一份可落地的最小权限实践清单。MySQL 的安全性控制并不是建个用户、给个权限这么简单。它本质上是一套分层级的访问控制矩阵由五张系统权限表承载配合连接层的认证方式、密码强度策略、角色机制共同组成。理解了这个矩阵的匹配顺序你才能预判任何一条 GRANT 或 REVOKE 最终会产生什么效果——这一点在排查为什么这个账号还是能删表这类问题时是决定性的。2. 用户与权限的底层存储先搞清楚数据存在哪2.1 mysql 库里的五张权限表各管什么MySQL 把权限信息全部存在系统库mysql里核心是下面这几张表理解它们的分工是理解整个权限体系的前提表名权限粒度典型适用场景mysql.user全局级所有库所有表超级管理员、全局只读账号mysql.db数据库级某个业务库的读写账号mysql.tables_priv表级只允许查某几张核心表mysql.columns_priv列级隐藏手机号、身份证等敏感列mysql.procs_priv存储过程/函数级只允许调用特定过程MySQL 8.0 之后还多了一张mysql.global_grants用来存动态权限比如BACKUP_ADMIN、ROLE_ADMIN、SYSTEM_VARIABLES_ADMIN这类不能写死在表结构里的权限。老版本 5.7 里这些权限是塞在 user 表的列里的8.0 拆出来单独一张表更合理扩展性也好。这里有个很多人不知道的细节mysql.user表的每一行代表一个用户名 主机名的组合而不是单纯的用户名。所以zhangsanlocalhost和zhangsan%在 MySQL 眼里是两个完全不同的账号各自有独立的密码和权限。头歌的判题脚本经常就是靠这一点来区分你建对了没有。2.2 权限校验的匹配顺序决定了你会不会被意外放行当一条 SQL 打过来MySQL 的服务层会按下面的顺序去合并权限先看user表把该账号的全局权限全部取出来再看db表看有没有针对目标库的授权记录有就并进来全局权限和库级权限是并集关系不是覆盖然后查tables_priv接着columns_priv最后如果是存储过程则查procs_priv所有层级的权限做并集得到这次连接实际拥有的权限集合。注意第 2 步这是最容易踩的坑。很多人以为库级权限会覆盖全局权限其实不会。如果你先用GRANT SELECT ON *.* TO app%给了全库只读后来想通过REVOKE SELECT ON *.*收回来却发现 app 账号还是能查——那就得检查一下db表里是不是还残留着一条库级授权。权限只做加法收权必须精确到当初授出去的那一层。另外主机名的匹配也有优先级localhost精确匹配 具体 IP 网段通配 %全通配。同一台机器上可能同时存在多条针对同一个用户名、但主机名不同的记录MySQL 会挑最精确的那条来用。这一点在排查同一个用户从不同机器连过来权限不一样的问题时特别关键。2.3 什么时候真的需要 FLUSH PRIVILEGES几乎所有 MySQL 教程都会写一句改完权限记得 FLUSH PRIVILEGES这句话其实只说对了一半。GRANT、REVOKE、CREATE USER、DROP USER这些 DDL 语句MySQL 内部会自动同步内存中的权限缓存根本不需要手动刷新。真正需要FLUSH PRIVILEGES的只有一种情况你用INSERT、UPDATE、DELETE这类 DML 语句直接改了mysql.user或mysql.db表的数据。比如你在 5.7 里手动往 user 表插一行来建账号那必须刷新才生效。所以别再无脑加FLUSH PRIVILEGES了。头歌判题有时候会因为多余的语句产生额外输出而干扰比对虽然多数脚本不会但养成精确使用命令的习惯没坏处。3. 账号管理实操创建、改密、删除、锁定3.1 CREATE USER 的完整参数拆解先看一条标准的建号语句CREATE USER bookapp192.168.10.% IDENTIFIED WITH mysql_native_password BY Bk2024#app WITH MAX_QUERIES_PER_HOUR 1000 MAX_CONNECTIONS_PER_HOUR 60 MAX_USER_CONNECTIONS 20 PASSWORD EXPIRE INTERVAL 90 DAY ACCOUNT UNLOCK;拆开看每个部分bookapp192.168.10.%是账号标识主机部分支持%和_通配%匹配任意长度字符_匹配单个字符IDENTIFIED WITH mysql_native_password BY ...指定认证插件并设置密码。MySQL 8.0 默认插件是caching_sha2_password老客户端比如 5.7 时代的某些驱动连不上所以生产上对接老系统时显式降级到mysql_native_password反而更稳MAX_QUERIES_PER_HOUR等四个资源限制参数是防止某个账号被程序写死循环后把数据库拖垮的保险丝值设成 0 表示不限制PASSWORD EXPIRE INTERVAL 90 DAY让密码 90 天后过期强制轮换ACCOUNT UNLOCK显式解锁避免从别的模板复制过来时带着锁定状态。头歌实验里如果只考基础建号你可以省略资源限制部分但如果任务描述里提到了限制每小时查询次数之类的字眼那这些参数一个都不能漏。判题脚本校验的是mysql.user表里max_questions、max_connections这些列的数值你少写一个对应的列就是默认值 0直接判错。3.2%和localhost的区别是新手第一个大坑applocalhost和app%到底差在哪localhost在 MySQL 里是个特殊值它走的是 Unix Socket 或者本地回环而且只匹配本机连接。%则匹配任意主机包括本机。坑在于MySQL 的匹配优先级里localhost高于%所以如果你本机已经存在applocalhost然后用app%从本机去连MySQL 会优先用localhost那条记录用的是 localhost 那套密码和权限。结果就是你在%上做的授权在本机测试时看不到效果一到远程机器上又正常了。这种现象非常折磨人因为它时好时坏。我的建议是如果业务需要从任意机器连接就统一用%别同时留localhost的记录。如果确实需要保留那两边的密码和权限必须保持一致或者干脆明确划清哪些机器走哪个账号。3.3 密码强度策略5.7 是插件8.0 是组件MySQL 的密码强度校验在 5.7 和 8.0 里安装方式完全不同这个差异让不少人在头歌上卡关。5.7 的写法是插件INSTALL PLUGIN validate_password SONAME validate_password.so;8.0 改成了组件语法是INSTALL COMPONENT file://component_validate_password;装完之后看当前策略SHOW VARIABLES LIKE validate_password%;关键参数及推荐值参数含义MEDIUM 策略下的典型值validate_password.policy强度等级MEDIUMvalidate_password.length最小长度8validate_password.mixed_case_count大小写字母各至少几个1validate_password.number_count数字至少几个1validate_password.special_char_count特殊字符至少几个1改策略用SET GLOBAL validate_password.policy MEDIUM;改完立即对后续的新建用户和改密生效。这里有个实操心得MEDIUM 策略会拒绝把用户名本身包含在密码里。比如账号叫bookapp密码设成Bookapp123会直接报错。写实验报告或者做测试数据的时候密码里千万别带账号名。3.4 账号锁定与密码过期的组合用法比起直接DROP USER删号把账号锁掉是一种更温柔的下线方式因为权限配置还留着将来需要恢复时ALTER USER ... ACCOUNT UNLOCK一句就回来了。ALTER USER bookapp192.168.10.% ACCOUNT LOCK; ALTER USER bookapp192.168.10.% PASSWORD EXPIRE;第二条让密码立即过期用户下次登录时必须先改密码否则只能连上但执行不了任何操作。这个组合在下线离职员工账号时特别好用——先锁号观察一周没有异常访问再说必要时能立刻恢复。4. 权限授予与回收GRANT / REVOKE 的正确姿势4.1 权限粒度清单别只会 ALLALL PRIVILEGES确实方便但它包含了DROP、FILE、SHUTDOWN这类高危权限业务账号上用它是灾难。下面是我常用的一份粒度速查数据操作类SELECT、INSERT、UPDATE、DELETE结构变更类CREATE、ALTER、DROP、INDEX、CREATE VIEW执行类EXECUTE调存储过程、CREATE ROUTINE、ALTER ROUTINE管理类CREATE USER、GRANT OPTION、RELOAD、PROCESS、SUPER文件类FILE能读写服务器文件系统危险等级极高。一个典型的只读报表账号只需要SELECT一个正常业务账号通常是SELECT, INSERT, UPDATE, DELETE连DROP都不该给——表结构的变更应该走发布流程用专门的迁移账号执行。4.2 最小权限的落地写法以图书管理系统为例我一般这样开号CREATE USER book_read% IDENTIFIED BY Rd2024#01; GRANT SELECT ON bookdb.* TO book_read%; CREATE USER book_write% IDENTIFIED BY Wr2024#02; GRANT SELECT, INSERT, UPDATE, DELETE ON bookdb.* TO book_write%; CREATE USER book_view% IDENTIFIED BY Vw2024#03; GRANT SELECT (id, title, author, price) ON bookdb.books TO book_view%;第三条就是列级权限的实际用法。授权的时候把列名写在小括号里账号就只能查这几列。这招用来对付审计要求比如身份证、手机号字段不允许开发直接查特别有效比建视图还省事因为它不需要改动应用层的表名。注意列级权限一旦启用SELECT *会直接报权限不足。所以列级授权只适合应用层明确指定列名的场景用 ORM 全字段映射的框架要小心。4.3 WITH GRANT OPTION 的危险性GRANT ... WITH GRANT OPTION的意思是我给你的权限你也可以转授给别人。听上去很人性化实际上是权限失控的源头。设想一下你给 A 部门的管理员授了bookdb的读写权限并带上 GRANT OPTION然后 A 管理员把权限转授给了实习生实习生又转授给了他自己的测试账号。等你想要回收时REVOKE只能收回你直接授出去的那一层A 管理员转授出去的那一堆记录还留在权限表里你得一条条找出来清。更麻烦的是如果中途有账号被删了它转授出去的权限会变成孤儿记录排查起来非常痛苦。我的原则是GRANT OPTION 只给数据库管理员账号业务账号一律不给。如果确实需要某个团队自己管理权限用角色机制下一节讲来代替权限转授边界清晰得多。4.4 REVOKE 的精确匹配要求REVOKE的语法必须和当初GRANT的粒度完全对应否则会报 There is no such grant defined for user。-- 授的是库级 GRANT SELECT, INSERT ON bookdb.* TO book_write%; -- 回收也必须按库级回收 REVOKE INSERT ON bookdb.* FROM book_write%; -- 下面这种写法会报错因为当初并没授过表级的 INSERT REVOKE INSERT ON bookdb.books FROM book_write%;另外REVOKE ALL PRIVILEGES, GRANT OPTION FROM user这种一次性清空的写法很常用但要注意它只回收权限账号本身还在。想连账号一起删掉得再补一句DROP USER。回收完记得验证SHOW GRANTS FOR book_write%;这条命令会把该账号当前所有生效的授权列出来包括通过角色继承的会额外标注USAGE行是最可靠的核对手段。头歌判题前我一定会先跑一遍SHOW GRANTS确认状态符合预期再提交。5. 角色MySQL 8.0 的权限批量管理方案5.1 角色的本质是没有登录能力的账号MySQL 8.0 引入的角色ROLE在我看来就是把一组权限打包起个名字。它的创建语法和建用户几乎一样CREATE ROLE role_readonly; GRANT SELECT ON bookdb.* TO role_readonly; GRANT role_readonly TO book_read%;注意最后一句GRANT 角色 TO 用户和GRANT 权限 TO 用户语法结构一样但含义是把这个角色包挂到用户身上。角色本身没有密码不能被直接登录只能被授予给用户或者其他角色角色可以嵌套。角色的价值在于当你有 20 个只读账号时权限变更只需要改角色定义一次所有挂着这个角色的账号立即生效。如果不用角色你得写 20 条 GRANT 语句还容易漏。5.2 默认角色与激活状态角色授给用户之后默认是不激活的。用户登录后需要手动SET ROLE role_readonly;才能拿到权限。如果希望登录时自动激活得设默认角色SET DEFAULT ROLE role_readonly TO book_read%;或者用SET DEFAULT ROLE ALL TO user把所有已授角色设为默认。还可以通过系统变量控制全局行为SET GLOBAL activate_all_roles_on_login ON;打开这个开关后用户登录时所有被授予的角色自动激活。这在纯业务环境里很方便但会削弱最小权限的意图因为用户拿到的角色数可能超出当前任务需要。我倾向于保持默认关闭由应用在连接后按需激活。有一个容易被忽略的点角色激活后CURRENT_ROLE()能查到当前角色SHOW GRANTS FOR CURRENT_USER会同时显示直接权限和角色带来的权限。排查权限到底从哪来的时候这两条命令配合使用效率很高。5.3 角色与视图谁能替代谁很多老教程讲权限隔离时会推荐用视图封装敏感列比如建一个只包含非敏感字段的视图把视图的查询权限给出去。这个思路在 5.7 时代是主流因为那时候没有角色列级权限管理起来也麻烦。到了 8.0我的选择顺序是先考虑角色再考虑列级权限最后才考虑视图。理由是视图会引入额外的 SQL 层某些查询条件下尤其是带聚合和 JOIN 的性能会明显下降而且视图定义一旦被改所有依赖它的账号都会受影响。角色和列级权限都是纯粹的权限声明不改变数据访问路径出问题的面小得多。当然如果需求是把多张表的关键字段拼成一张宽表给报表用那视图还是不可替代的。工具没有绝对优劣看场景。6. 头歌实验通关实录关键步骤与判题陷阱6.1 关卡任务的通用拆解路径头歌的 MySQL 安全性控制实验任务描述通常长这样创建用户 xxx密码为 xxx授予其对 xxx 库的 xxx 权限然后回收 xxx 权限。看着简单但每一步都有陷阱。我的处理流程固定为四步先看清楚任务要求的是哪个粒度。是库级db.*还是表级db.table是全局*.*还是仅某列这个判断错了后面全错。确认用户名和主机名的完整写法。任务里给出的账号格式往往是userhost或userhost照抄就行别自作主张改成%。头歌判题脚本很可能就是精确匹配mysql.user表里的Host字段值。执行后立刻验证。用SELECT user, host FROM mysql.user WHERE userxxx;和SHOW GRANTS FOR xxxhost;两条命令确认状态。按需清理环境。有些关卡的判题是累加式的上一关建的账号如果没删掉会影响这一关的计数校验。6.2 判题脚本最常卡你的几个点踩过一圈之后我总结出下面这份高频扣分点清单扣分点具体表现规避方法主机名不匹配要求localhost写成了%严格照抄任务描述中的主机部分权限粒度错误要求表级写成了库级反复确认db.table还是db.*残留旧授权上一关的权限没收干净提交前SHOW GRANTS核对密码策略冲突密码太简单建号失败先查validate_password参数认证插件不匹配8.0 默认插件导致判题端连不上必要时显式指定mysql_native_password语句未提交编辑器里写了但没执行确认每行都出现Query OK其中残留旧授权是最隐蔽的。因为判题脚本通常是查mysql.db或mysql.tables_priv的记录条数如果上一关的测试账号还在记录数就对不上。养成每个关卡结束后清理测试账号的习惯能省掉大量莫名其妙的失败。6.3 我自己的调试节奏分享一个我反复验证过的调试节奏先在命令行里用root把整条 SQL 手敲一遍看有没有语法报错确认无误后再DROP USER把测试账号清掉重新按任务要求完整执行一遍最后SHOW GRANTS核对。整个过程不超过两分钟但能避免 90% 的无效提交。还有一种情况值得单独说有些关卡的判题是在独立会话里执行的如果你的权限修改还停留在当前会话的缓存里没落盘判题端可能读到旧状态。前面说过 GRANT/REVOKE 会自动同步但如果你是用 DML 改的权限表就必须手动FLUSH PRIVILEGES。所以看到任务描述里出现直接修改权限表的字眼刷新语句一定要补上。7. 常见报错与排查速查表下面这张表是我在实际操作和陪别人做实验时整理出来的按报错信息直接查就行。报错信息根本原因解决思路Access denied for user密码错、主机不匹配或账号被锁查mysql.user中该账号的account_locked和 host 值There is no such grant definedREVOKE 的粒度与 GRANT 不一致用SHOW GRANTS看清原授权粒度后再回收Your password does not satisfy the current policy密码强度不达validate_password要求提高复杂度或按任务要求调整策略参数operation CREATE USER failed for ...账号已存在或权限不足先DROP USER IF EXISTS或换高权限账号操作Table mysql.user doesnt existMySQL 8.0 权限表结构变更或版本异常确认版本8.0 的认证信息在authentication_string列Plugin mysql_native_password is not loaded8.0.34 该插件默认未启用用INSTALL PLUGIN加载或改用caching_sha2_passwordCannot revoke all privileges for one of the requested users试图回收自己都没授过的权限检查是否存在通过角色继承的权限排查这类问题的通用思路是先定位层级再看具体记录。定位层级靠SHOW GRANTS看具体记录靠直接查mysql库的权限表。这两招结合起来几乎没有排查不出来的权限问题。提示查权限表需要先用高权限账号连上。如果连高权限账号都进不去那就得走跳过权限验证启动 改密码的急救流程了这个流程涉及服务器配置文件修改属于运维范畴实验环境里一般用不到。8. 生产环境里比实验更重要的几件事8.1 最小权限不是口号是一条条抠出来的实验里为了过判题我们经常把权限给得比较宽。但真实生产环境反过来——宁可多花半小时拆权限也不要图省事给ALL。我通常按服务维度拆账号而不是按人维度。比如一个电商系统订单服务一个账号、用户服务一个账号、报表服务一个账号每个账号只碰自己相关的库表。这样即使某个服务的配置泄露影响范围也被限制在那几张表里。按人拆账号的问题是人员流动频繁账号交接和清理成本高得离谱。8.2 审计日志和慢查询是权限体系的监控探头光有权限控制是不够的你还得知道谁在什么时候用了什么权限。MySQL 的通用查询日志general log和审计插件能记录所有连接和语句。生产环境不建议开 general logI/O 开销大但审计类插件是值得上的。另外慢查询日志虽然主要用来调性能但它也是排查某个账号在做异常批量操作的线索来源。我遇到过账号被误用后一次删掉几十万行的情况最早就是从慢查询日志里发现那条全表 DELETE 的。8.3 备份账号必须单独隔离最后强调一个容易被忽视的点备份账号千万不要和业务账号共用。备份工具需要SELECT、LOCK TABLES、RELOAD、REPLICATION CLIENT这些权限其中RELOAD和LOCK TABLES对业务是有影响的。如果业务账号带着这些权限跑某个逻辑写错的情况下可能把整个库锁住。正确的做法是给备份工具单独开一个只允许从固定 IP 连接的账号权限限定在备份相关的最小集合上并且和业务账号分属不同的密码策略组——备份账号的密码可以更长、更换频率可以更低因为它的连接来源是固定的。这条经验是我在一次线上事故复盘里学到的当时备份脚本用的账号和某个后台服务共用后台服务上线新功能时误用了这个账号做了一批批量更新结果备份任务被长时间阻塞影响了当晚的备份窗口。分开之后这类耦合问题就再也没出现过。
返回列表