最近帮一个团队排查线上连接问题,绕了一圈发现不是网络也不是配置的锅,而是新建的账号压根没拿到权限。类似这种"MySQL用户管理"的坑,几乎每个用MySQL的人都会踩一遍。今天就把这块内容系统地整理一遍,从用户创建到权限分配,从常用命令到生产环境的实操套路,该说的细节都会说到,尤其是那些官方文档里不会写明白的坑。
这篇文章不是什么从入门到精通的大部头,就是把"MySQL用户管理"这件事讲透,让开发、运维、甚至自己搭环境玩的人,看完都能直接上手。
1. MySQL用户管理的整体设计思路
1.1 先搞清楚:MySQL的"用户"到底是谁
很多初学者对MySQL用户的理解就是"一个名字加一个密码",其实不对。MySQL里一个用户的完整身份是'用户名'@'主机'的组合,用户名只是前半部分,后面那个主机限制同样关键。
举个例子,你创建了'app'@'%',这个账号可以从任何主机连接。但如果你创建的是'app'@'localhost',那就只有本机才能用这个账号连上来。很多人排查"账号密码明明对,为什么连不上"的问题,八成就是栽在这个主机限定上。
另外,MySQL的用户认证信息存储在系统库mysql下的user表里,密码不是明文,而是经过哈希处理的摘要串。你可以查询这张表来确认账号情况:
SELECT user, host, plugin, account_locked FROM mysql.user;如果哪天你发现"用户列表"里出现一堆root@localhost、root@127.0.0.1、root@::1,不用慌,这不是重复,而是MySQL把不同来源的root连接都当成了独立条目来看待。
1.2 权限体系才是用户管理的核心
用户管理如果只是建账号、设密码,那就太简单了。真正麻烦的是权限体系——你可以把权限想象成一把把钥匙,每一把钥匙能打开不同的门。
MySQL的权限是分层级的,从大到小排列:
- 全局权限:对整个MySQL实例生效,写在
mysql.user表里,授权语句里通常写成ON *.* - 库级权限:对某个数据库生效,写在
mysql.db表里,授权语句里形如ON db_name.* - 表级权限:对某张表生效,写在
mysql.tables_priv表里,写法是ON db_name.table_name - 列级权限:对某个表的某几列生效,写在
mysql.columns_priv表里,精确到列名 - 存储过程和函数权限:写在
mysql.procs_priv表里
授权的时候,系统会按照这个层级去匹配。一个用户即使没有全局权限,只要在某个库上有库级权限,就能操作这个库里的表。
理解了这个分层结构,你才能明白为什么有些账号能看所有库,有些账号只能看一个库里的几张表。市面上那些MySQL可视化工具,其实也就是在帮你拼GRANT语句,底层逻辑还是这一套。
1.3 为什么用户管理要提前规划
我在实际工作中见过太多"先跑起来再说"的项目:开发环境共用root账号,生产环境也共用root账号,结果某天一个误操作把整个数据库删了,连数据恢复机制都没来得及建立。
用户管理最核心的目标是"最小权限原则",也就是说,一个账号只给它完成工作所必需的最小权限。这样做有三个好处:
- 降低误操作风险。没给DELETE权限的账号,再怎么手滑也删不掉数据。
- 提升安全性。数据库被入侵时,攻击者能拿到的权限上限被限制住了。
- 方便排查问题。每个账号对应一个业务模块,出了问题能快速定位是谁干的。
这套规划最好在数据库初始化阶段就做。如果你接手的是一个已经跑了几年的老库,也没关系,可以后续慢慢梳理和收敛权限。本文后面会专门写一套生产环境的账号规划模板。
2. 用户创建与管理:不出错的基础操作
2.1 创建用户的完整语法与常见写法
创建用户的标准语法在MySQL 5.7和8.0里略有差异,但核心逻辑是相同的。最基本的写法是:
CREATE USER '用户名'@'主机' IDENTIFIED BY '密码';这里有几个关键点需要详细说说:
主机部分怎么写。'localhost'限定只能本机连接;'192.168.1.%'限定只能从192.168.1.x网段连接;'%'代表不限制主机,任何IP都能连。如果业务服务器和数据库服务器分离部署,建议精确到IP或网段,而不是偷懒一律用%。
密码的存放方式。8.0版本默认使用了caching_sha2_password插件的认证方式,而5.7时代默认是mysql_native_password。很多老客户端连不上MySQL 8.0就是因为不支持新的认证插件,这时候需要在创建用户时指定:
CREATE USER 'app'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';不过这里我建议尽量升级客户端而不是降级认证方式,毕竟caching_sha2_password更安全。
常见写法整理一下,大致如下:
-- 最简单的创建方式 CREATE USER 'dev'@'localhost' IDENTIFIED BY 'Dev@123456'; -- 指定认证插件(兼容老客户端) CREATE USER 'app'@'192.168.1.%' IDENTIFIED WITH mysql_native_password BY 'App@666888'; -- 创建用户并同时授权(开发环境常用,生产环境慎用) GRANT ALL PRIVILEGES ON mydb.* TO 'dev'@'localhost' IDENTIFIED BY 'Dev@123456';注意,MySQL 8.0 中GRANT语句不再支持在授权的同时创建用户,需要先CREATE USER再GRANT。如果你用的是8.0,用上面第三个语句会直接报语法错误。
2.2 查看用户与权限信息
创建完用户之后,想确认一下创建得对不对,有两个查询方向:
第一,看用户的账号信息:
SELECT user, host, plugin, account_locked, password_expired FROM mysql.user;第二,看用户的权限明细:
-- 查看某个用户有哪些权限 SHOW GRANTS FOR 'app'@'192.168.1.%'; -- 如果你不确定主机怎么写,先查一下 SELECT user, host FROM mysql.user WHERE user = 'app';SHOW GRANTS这个命令一定要养成习惯。排查权限问题时,它比任何工具都直接。它会把该用户在当前节点上拥有的所有权限一条一条列出来,包括角色信息。
有一个容易忽略的点:SHOW GRANTS FOR 'abc'和SHOW GRANTS FOR 'abc'@'%'结果可能完全不同,因为前者会默认补上'abc'@'%',但实际生效的账号可能是'abc'@'localhost'。所以查询的时候最好把主机部分写全了。
另外,如果你想查看当前会话用户的权限,可以直接:
SHOW GRANTS;还有一点,8.0版本引入了角色(Role)概念后,直接看SHOW GRANTS可能只会显示角色名称,而不是展开后的具体权限。你需要加一句:
SHOW GRANTS FOR 'app'@'%' USING 'read_only_role';或者干脆设置登录时自动激活所有角色,这个后面章节细说。
2.3 修改密码与密码过期策略
修改密码的常用语法在8.0和5.7里基本一致:
-- 修改当前登录用户的密码 ALTER USER USER() IDENTIFIED BY '新密码'; -- 修改指定用户的密码 ALTER USER 'app'@'%' IDENTIFIED BY '新密码';如果是5.7版本,还可以用SET PASSWORD FOR 'app'@'%' = PASSWORD('新密码');,但8.0里已经移除了PASSWORD()函数,所以统一推荐ALTER USER写法。
这里必须提醒一个老坑:你有权限改别人的密码,前提是你自己拥有对这个用户的CREATE USER或UPDATE权限,否则会收到ERROR 1044 (42000): Access denied的报错。
密码过期策略在生产环境很实用。你可以让某个账号每隔一段时间必须改一次密码:
-- 让密码90天后过期 ALTER USER 'app'@'%' PASSWORD EXPIRE INTERVAL 90 DAY; -- 立刻让密码失效(下次登录必须改密码) ALTER USER 'app'@'%' PASSWORD EXPIRE; -- 取消过期限制 ALTER USER 'app'@'%' PASSWORD EXPIRE NEVER;如果你不想让数据库里的账号密码长期不动,又怕大家忘记改,可以全局设置默认密码过期策略,在my.cnf里加:
default_password_lifetime = 902.4 删除与锁定用户
删用户很简单:
DROP USER 'app'@'%';但有些情况下你只是想临时停用一个账号,而不是删除,比如离职人员交接期、某个应用正在维护中。这时候用锁定更稳妥:
-- 锁定账号 ALTER USER 'app'@'%' ACCOUNT LOCK; -- 解锁账号 ALTER USER 'app'@'%' ACCOUNT UNLOCK;这个功能我经常用在"离职员工交接但发现还有定时任务在跑"的场景。先锁定,观察一段时间确认没有任务依赖,再删除,比直接DROP USER安全得多。
另外提醒一下,DROP USER一个不存在的用户会报ERROR 1396 (HY000): Operation DROP USER failed。结合前面提到的,先查一下mysql.user表里账号的完整 host 信息再删,百试百灵。
3. 权限授权与回收:掌握最小权限原则
3.1 常见权限类型与适用场景
MySQL的权限类型非常多,但日常工作中常用的就那么几种。我整理了一个表格,方便快速对照:
| 权限名称 | 适用范围 | 说明 |
|---|---|---|
| SELECT | 库/表/列 | 查询数据,只读账号必备 |
| INSERT | 库/表/列 | 插入数据 |
| UPDATE | 库/表/列 | 更新数据 |
| DELETE | 库/表/列 | 删除数据 |
| CREATE | 库/表 | 创建库或表 |
| DROP | 库/表 | 删除库或表,高危权限 |
| ALTER | 表 | 修改表结构 |
| INDEX | 表 | 创建或删除索引 |
| REFERENCES | 表/列 | 外键约束的参照权限 |
| CREATE VIEW | 视图 | 创建视图 |
| SHOW VIEW | 视图 | 查看视图定义 |
| CREATE ROUTINE | 存储过程/函数 | 创建存储过程、函数 |
| ALTER ROUTINE | 存储过程/函数 | 修改或删除存储过程、函数 |
| EXECUTE | 存储过程/函数 | 执行存储过程、函数 |
| PROCESS | 全局 | 查看所有线程信息,DBA排查慢查询常用 |
| RELOAD | 全局 | 执行 FLUSH 操作 |
| REPLICATION SLAVE | 全局 | 主从复制从库拉取二进制日志需要 |
| REPLICATION CLIENT | 全局 | 查看主从状态 |
| SUPER | 全局 | 超级权限,8.0中被拆分精简,高权限操作 |
| ALL PRIVILEGES | 全局/库/表 | 除 GRANT OPTION 以外的所有权限 |
需要注意的是,GRANT OPTION是单独的权限,它的作用是把授权的能力授予用户,也就是说"允许你把拥有的权限转授给其他账号"。默认GRANT ALL并不会授予GRANT OPTION,除非你显式指定。
3.2 GRANT与REVOKE的语法与常见组合
授权和回收权限的基本语法:
-- 授权 GRANT 权限列表 ON 对象 TO '用户'@'主机'; -- 回收权限 REVOKE 权限列表 ON 对象 FROM '用户'@'主机';举个例子,要给一个只读账号授权:
GRANT SELECT ON mydb.* TO 'readonly'@'%';要给应用账号授权增删改查:
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app'@'%';要给某个表单独的权限:
GRANT SELECT ON mydb.orders TO 'report'@'%';如果想一次性把所有权限都给某个库(开发环境常用):
GRANT ALL PRIVILEGES ON mydb.* TO 'dev'@'localhost';但真不建议动态地把ALL PRIVILEGES开到生产环境去,除非这个库就是私人玩具。
回收权限要注意一个点:如果你之前给了ALL PRIVILEGES,想回收部分权限,不能只写REVOKE SELECT,那样会报REVOKE ALL PRIVILEGES的写法问题。正确做法是把之前授权的内容完整地回收掉,再重新授权需要的部分。比如:
-- 错误的做法(可能报错或与预期不符) REVOKE ALL PRIVILEGES ON mydb.* FROM 'dev'@'localhost'; -- 正确的做法 REVOKE SELECT, INSERT, UPDATE, DELETE ON mydb.* FROM 'dev'@'localhost';这里想强调一下,MySQL的权限表按层级独立记录,授权和回收都是增量叠加。比如你先授权了SELECT ON mydb.*,又授权了SELECT ON mydb.orders,那么用户对orders表有两层SELECT权限,但后面回收mydb.*的SELECT时,表级权限依然保留。
3.3 WITH GRANT OPTION:授权链的风险
WITH GRANT OPTION是权限管理里一个挺危险但是又经常被误解的选项。
GRANT SELECT ON mydb.* TO 'user_a'@'%' WITH GRANT OPTION;执行完后,user_a不仅拥有mydb库的SELECT权限,还能把自己拥有的权限转授给其他人。也就是说它可以创建一个新用户,并给这个新用户赋mydb.*的SELECT权限。
这在创建DBA类账号时可能有用,但给普通业务账号加上就非常危险。万一账号泄露,攻击者可以自己创建账号,你回收了原账号也无济于事,因为新的账号还在。
所以我的建议很明确:业务应用账号一律不要加WITH GRANT OPTION,哪怕是管理员账号,也建议单独用一个管理账号,不要和业务账号混在一起。
3.4 用角色(Role)简化权限管理
MySQL 8.0开始支持角色(Role),这是一个非常实用的功能,本质上是把一组权限打包成一个集合,然后把集合赋给用户。好比公司里"开发"是一个角色,拥有开发需要的一组权限;"DBA"是另一个角色,拥有数据库管理需要的一组权限。
创建角色的基础操作:
-- 创建角色 CREATE ROLE 'read_only_role'; -- 给角色授予权限 GRANT SELECT ON mydb.* TO 'read_only_role'; -- 把角色授予用户 GRANT 'read_only_role' TO 'app_read'@'%';需要注意的是,角色授予用户后,默认不会自动激活,需要设置:
-- 设置某用户登录后激活所有角色 SET DEFAULT ROLE ALL TO 'app_read'@'%'; -- 或者全局开启:所有用户登录时自动激活所有角色 SET GLOBAL activate_all_roles_on_login = ON;推荐后一种方式,省心省力。否则你会遇到一个很奇怪的场景:SHOW GRANTS里能看到角色,但实际查询时权限不生效,就是因为角色没有被激活。
角色还有一个好处是批量管理方便。比如你新招了一个数据分析师,他需要和之前的分析师一样的权限,只需要:
GRANT 'analyst_role' TO 'new_user'@'%'; SET DEFAULT ROLE ALL TO 'new_user'@'%';不用再一条条地GRANT SELECT/INSERT/UPDATE...,省了不少事。
4. 生产环境用户管理实操套路
4.1 按业务场景建立账号
把业务和账账号类型理清楚之后,你在生产环境的用户管理才不容易乱。我这边的经验是建一个账号规划表,在一个新环境初始化时直接照着执行。
拿一个比较典型的Web应用系统举例,通常可以规划成:
| 账号类型 | 用户名 | 主机限制 | 权限范围 | 说明 |
|---|---|---|---|---|
| 应用主账号 | app_web | 应用服务器IP网段 | 业务库的SELECT/INSERT/UPDATE/DELETE | 给后端代码连接用 |
| 只读分析账号 | readonly_report | 报表服务器IP | 业务库的SELECT | 给BI报表、数据导出用 |
| 备份账号 | backup_ops | localhost或备份机IP | RELOAD、SELECT、REPLICATION CLIENT等 | 给备份工具专用 |
| DBA管理账号 | dba_admin | 跳板机IP | 全局权限+GRANT OPTION | 给DBA日常维护用 |
| 结构变更账号 | ddl_deploy | CICD服务器IP | ALTER、CREATE、INDEX、DROP(风险操作) | 给自动化发版脚本用,操作完回收 |
实操时,可以把这些CREATE USER和GRANT语句都写到初始化脚本里,作为一套标准交付物。新环境建库时直接执行,比手动一个个点省心多了。
开发环境可以适当放宽权限,但也不能反正就是内部用就乱来。我见过太多开发环境的root密码全网公用的案例,某天一个测试脚本误连接了生产库,后果自己体会。
4.2 主机限制与远程访问安全
'%'是很多新手喜欢用的通配符,因为省事。但生产环境里主机限制是一道重要的安全防线。举个例子:
CREATE USER 'app'@'%' IDENTIFIED BY 'password';这条指令创建了一个可以从任何IP连接的账号。万一密码泄露,攻击者可以从任何地方尝试连接,风险极大。更合理的方式是绑定IP或网段:
CREATE USER 'app'@'192.168.10.%' IDENTIFIED BY 'password';这样只有来自192.168.10.x网段的机器能连。就算是开发环境,也建议用类似'10.10.%'的段位限制,不要直接开成%。
MySQL还有一个容易忽略的坑:如果你只创建了'app'@'%',但客户端本机用-h localhost连接时可能不会匹配到%,这时候需要专门创建一个'app'@'localhost'。很多人在Docker里跑MySQL时,容器内连接死活认证失败,就是因为容器内的连接来源和外部连接来源被系统当成不同的用户匹配了。
4.3 密码安全与validate_password插件
MySQL 8.0默认启用了密码校验组件validate_password,它的作用就是检查你设置的密码是否符合安全策略。默认情况下,密码必须包含大小写字母、数字和特殊字符,长度也有最低要求。
查看和修改密码策略相关的参数:
SHOW VARIABLES LIKE 'validate_password%';输出里有几个关键参数:
validate_password.length:密码最小长度validate_password.policy:密码强度策略(LOW/MEDIUM/STRONG)validate_password.number_count:数字至少出现次数validate_password.mixed_case_count:大小写字母至少出现次数validate_password.special_char_count:特殊字符至少出现次数
对于测试环境,如果你觉得这个组件太烦,可以调整参数:
SET GLOBAL validate_password.policy = LOW; SET GLOBAL validate_password.length = 6;不过生产环境还是建议保持默认的强密码策略,或者往上调,不建议降级。
还有一个经验之谈:正式环境不要用一个密码打天下,每个账号尽量独立密码,定期更换。像Vault、AWS Secrets Manager这类密钥管理工具可以自动化密码轮换,如果有条件可以引入,没有的话也可以做一个定期提醒的脚本。
4.4 Docker MySQL实例中的用户管理注意点
现在用Docker跑MySQL的人越来越多了,用户管理有一些容器环境特有的坑,单独拿出来说一下。
用docker run启MySQL时,可以通过环境变量来初始化用户:
docker run -d \ --name mysql \ -e MYSQL_ROOT_PASSWORD=root_password \ -e MYSQL_DATABASE=myapp \ -e MYSQL_USER=app \ -e MYSQL_PASSWORD=app_password \ -p 3306:3306 \ mysql:8.0这里MYSQL_USER和MYSQL_PASSWORD会在数据库初始化时自动创建一个'app'@'%'账号,并且只授予MYSQL_DATABASE这个库的全部权限。
注意,这个机制只在数据目录首次初始化时生效。如果你挂载了已有的数据卷,再改MYSQL_USER是没用的。很多人在Docker里改了环境变量发现账号没变,就是这个原因。
Docker容器里忘记密码后的操作顺序是:
- 停止容器,把启动命令改成增加
--skip-grant-tables参数(有些镜像会默认忽略这个参数,需要加--skip-grant-tables=1)。 - 重启容器,此时无需密码就能进MySQL。
- 修改密码,然后去掉跳过授权参数重启容器。
这个处理逻辑和普通环境一样,只是涉及Docker的命令层操作。具体细节在下一章讲"密码忘记"问题时会再展开。
容器和宿主机之间的权限匹配也要注意。如果你从宿主机用Navicat连接MySQL容器,连接来源在MySQL看来是宿主机的IP,不是容器内的IP,所以用户的主机限制要根据实际访问路径来确定,不是容器启动参数里写什么就一定唯一。
5. 常见问题与排查技巧实录
5.1 常见错误速查表
日常运维中,关于用户管理和权限的错误码大概就那么几个,我把常见的整理成一张速查表:
| 错误码 | 错误信息 | 常见原因 | 排查方向 |
|---|---|---|---|
| 1045 | Access denied for user | 密码错误,或主机被限制 | 检查密码、host匹配、认证插件 |
| 1044 | Access denied for database | 没有该库的访问权限 | 查看用户权限是否包含目标库 |
| 1130 | Host is not allowed to connect | 用户没有匹配该来源主机的记录 | 检查mysql.user里的host字段 |
| 1142 | command denied to user | 对某对象的特定操作没有权限 | 检查权限层级 |
| 1396 | Operation ... failed | 用户不存在或已存在 | 确认账号完整身份和状态 |
| 1862 | Your password has expired | 密码已过期 | 重置密码或修改过期策略 |
| 2002 | Can't connect to local MySQL server | socket文件或服务未启动 | 检查mysqld进程和socket路径 |
| 2013 | Lost connection during query | 网络中断或超时 | 检查网络、超时参数 |
我曾经被一个ERROR 1045卡住了一下午,最后发现是连接串里把%转义成了%25,导致主机匹配不上,排查了半天。
5.2 案例:Access denied 的排查流程
假设你现在遇到了这个报错:
ERROR 1045 (28000): Access denied for user 'app'@'192.168.1.50' (using password: YES)完整的排查思路是:
第一步,确认用户是否存在。在服务器上用root登录,执行:
SELECT user, host, authentication_string FROM mysql.user WHERE user = 'app';如果查询结果为空,说明账号没建对,或者你连接的数据库不是目标实例。如果查询结果里有app@%,但没有app@192.168.1.%,那就要考虑主机匹配问题。
第二步,确认密码是否正确。用mysql -u app -p在目标来源机上测试,或者换一个明确的主机限制试一下。如果换成localhost能连,远程连不上,那就是主机限制的原因。
第三步,确认认证插件是否兼容。老客户端连接8.0时经常遇到这个坑:
ERROR 2059 (HY000): Authentication plugin 'caching_sha2_password' cannot be loaded解决方法有两个:升级客户端驱动;或把用户的认证插件改为mysql_native_password。
第四步,确认mysql.user表里是否有多个匹配条目。MySQL在匹配用户时会按精确到模糊的顺序,'app'@'localhost'和'app'@'%'不是一回事,注意区分。
5.3 案例:权限明明给了,为什么还是没生效
这是另一个非常高频的坑。你执行了:
GRANT SELECT ON mydb.* TO 'app'@'%';但用app登录后查询还是提示无权限,或者SHOW GRANTS里能看到权限但实际执行SQL时被拒绝。
第一个要检查的是角色有没有激活。8.0里如果权限是通过角色授予的,而角色的默认状态是未激活,那实际生效的权限列表就是空的。检查方式:
SELECT CURRENT_ROLE();如果是空值,说明当前会话没有激活任何角色。可以执行:
SET ROLE ALL;或者直接设置登录时自动激活:
SET GLOBAL activate_all_roles_on_login = ON;第二个要检查的是sql_mode或大小写敏感问题。表名大小写、库名的大小写,在某些操作下会影响权限匹配。Linux上MySQL的表名默认区分大小写,Windows默认不区分,权限匹配时会受这个影响。
第三个是权限继承问题。前面说过,库级、表级、列级权限是分层记录的,某个权限在某一层没有,不表示在下一层有。比如有用户对mydb库有SELECT,但你对mydb.orders授予了更细的列权限,操作时总权限是取交集,不是取并集。
第四个容易被忽略的是连接池问题。应用连接池里老连接持有的是旧权限快照,修改权限后新连接生效,但老连接还在用旧权限。遇到"改完权限还是不行"的反馈,先让应用重启连接池试试。
5.4 日常维护建议与实践总结
写代码技术的人常犯一个毛病:只关注业务的实现,忽略了数据库账号层面的维护。但实际上,用户管理的健壮性直接决定了数据库的安全底线。
几点常规建议分享给大家:
建议一:定期审计账号。每季度做一次账号复盘,确认哪些账号还在用、哪些账号权限过大了。用一条SQL就能列出所有账号和最近状态:
SELECT user, host, plugin, account_locked, password_expired FROM mysql.user WHERE user NOT IN ('mysql.session', 'mysql.sys', 'root');把业务账号清点后,对应的负责人确认一遍,长期无人认领的直接锁定。
建议二:统一权限模板。把常用的权限组合做成脚本或SQL模板,比如"只读模板"、"业务读写模板"、"备份模板",需要时直接引用,减少临时发挥带来的错误。
建议三:保留变更记录。每次GRANT、REVOKE、CREATE USER操作都记录下来,方便事后审计。Git里放一个SQL变更目录是我推荐的做法,比在聊天记录里找历史靠谱得多。
建议四:权限回收要算时间账。给临时账号设一个明确的过期时间,到期后自动锁定或删除。写一个简单的定时任务去检查password_expired字段和账号的锁定状态,可以避免大量遗留账号堆积。
回到开头那个同事遇到的问题,最后查明就是GRANT给了错的主机,导致程序所在服务器无法匹配到权限。折腾了一上午,其实就是创建用户时多写了一个%和少写了一个网段的问题。MySQL用户管理这件事,平时看着简单,一出事都是火烧眉毛的场面。从账号规划、主机限制、密码策略、权限回收这几个方向做好基本功,大部分问题都能在设计阶段提前规避。