必须在应用层用 bcrypt、argon2 或 scrypt 等慢哈希算法存储密码,而非 MySQL 的 SHA2() 或 MD5();因后者计算过快、无内置盐管理、易出错,且 AES_ENCRYPT() 可逆、PASSWORD() 已废弃。

别在 MySQL 里用 MD5() 或 SHA2() 直接哈希密码——哪怕加了盐,也扛不住暴力破解。真正安全的密码存储,必须在应用层完成,用 bcrypt、argon2 或 scrypt 这类慢哈希算法。
为什么不能在 SQL 层做密码哈希
MySQL 的 SHA2()、MD5() 都是普通哈希,计算太快,GPU 一秒钟能跑上亿次。攻击者拿走数据库后,几小时就能撞出大量弱口令。更关键的是:SHA2() 不自带盐管理,你手动拼接 CONCAT(salt, pwd) 容易出错——比如盐值重复、没存盐、盐长度不足 16 字节,或者把盐和密码顺序写反。
另外,PASSWORD() 在 MySQL 8.0 已被移除;AES_ENCRYPT() 是可逆的,完全违背密码验证“只比对、不还原”的基本原则,一旦密钥泄露,所有密码秒解。
常见错误现象:
- 登录校验 SQL 写成
WHERE password = SHA2(?, 256),但表里存的是带固定盐的哈希,导致永远匹配失败 - 用
UUID()当盐插进表里,却没在查询时读出该盐再拼接,而是硬编码一个新UUID() - 字段类型设为
VARCHAR(64),但argon2id哈希输出超 120 字符,存一半被截断
应用层加密:选对算法 + 正确存字段
密码哈希不是“算一次就完事”,得靠标准库生成含盐、迭代、标识的完整哈希字符串。例如:
- Python 用
passlib.hash.argon2.using(rounds=4, memory_cost=65536) - Java Spring Security 用
Argon2PasswordEncoder或BCryptPasswordEncoder(12) - PHP 直接调
password_hash($pwd, PASSWORD_ARGON2ID)
这些函数输出的是自描述字符串(如 $argon2id$v=19$m=65536,t=4,p=1$...),里面已包含盐、轮数、算法版本,验证时传入原始密码和这个完整字符串即可,无需你手动拆解。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
对应数据库字段设计要宽松:
- 类型必须是
VARCHAR(255)(argon2最长可能超 190 字符) - 绝不能用
TEXT或BLOB,某些 ORM 对它们的默认行为不可控 - 不要额外建
salt列——盐已内嵌在哈希串里,多存就是冗余和隐患
如果非要用 MySQL 函数(仅限测试或遗留系统)
真没法改代码时,至少避开明显雷区:
- 禁用
MD5()和SHA1(),连演示都别用 - 强制每用户独立盐:用
UUID()生成,存在单独列(如salt VARCHAR(36)),插入时SHA2(CONCAT(salt, ?), 512) - 哈希结果存
VARCHAR(128),因为SHA2(..., 512)输出 128 字符十六进制 - 验证时先查出 salt,再拼接哈希,别在 WHERE 里重复计算——否则无法走索引,且 MySQL 5.7+ 对函数索引支持极差
注意:SHA2() 不抗侧信道,也不防时序攻击,纯属“比明文强一点”的权宜之计。
MySQL 用户认证密码 ≠ 业务密码
很多人混淆这两者。你用 ALTER USER 'app'@'%' IDENTIFIED WITH caching_sha2_password BY 'xxx'; 设置的是连接数据库的账号密码,由 MySQL 自己管,用的是 PBKDF2+AES 加密流程,安全级别够高。
但业务表里的 users.password 字段,是给终端用户登录你的 App 用的,它必须独立于数据库认证体系。你不能指望 MySQL 帮你验证用户输的密码是否正确——那得靠你自己的应用逻辑去比对哈希。
最容易被忽略的一点:就算用了 argon2,如果登录接口没做速率限制、没记录失败尝试、没强制 HTTPS,再强的哈希也挡不住暴力爆破。密码存储只是纵深防御里的一环,不是终点。

















