存哈希密码应使用 VARBINARY 而非 VARCHAR,因其原样保存字节、避免字符集转换与静默截断;bcrypt 推荐 VARBINARY(64),argon2id 推荐 VARBINARY(128),SHA2-256 原始字节用 VARBINARY(32)。
为什么 VARCHAR(255) 存哈希密码是错的
存密码哈希(比如 bcrypt、argon2 或 sha256 输出)时,用 varchar 不仅多余,还埋下隐性风险。哈希值是固定长度的二进制数据,不是文本——varchar 会触发字符集转换、排序规则比较、甚至意外截断(比如 mysql 的 utf8mb4 下一个字符占 4 字节,但你只按“字符数”分配长度)。更关键的是,某些哈希输出含不可见控制字符或字节序列,在 varchar 字段里可能被静默修改或报错。
实操建议:
- 一律改用
VARBINARY,它原样保存字节,不解释、不转换、不填充 -
bcrypt输出恒为 60 字符(ASCII),对应VARBINARY(60) -
argon2id默认编码后约 120 字符,推荐VARBINARY(128)留点余量 - 若用
SHA2-256+ Base64 编码,结果是 43 字符(含=填充),VARBINARY(44)足够;但更推荐直接存原始 32 字节 →VARBINARY(32)
VARBINARY 长度设太小会直接报错
MySQL 和 PostgreSQL 对 VARBINARY 都执行严格长度校验:插入超长值会直接报 Data too long for column,不会像 VARCHAR 那样自动截断(除非 SQL mode 开了 STRICT_TRANS_TABLES 关闭,但绝不该这么干)。
常见错误现象:
- 注册成功,登录时报
Invalid password—— 实际是密码哈希入库时被截断,比对永远失败 - 迁移旧用户数据时批量失败,日志里只有模糊的 “error 1406”
实操建议:
- 查清你用的加密库实际输出字节长度,而不是“看起来多少字符”。例如 Python 的
bcrypt.hashpw()返回bytes,len(hash)就是真实字节数 - 留 10% 余量:比如
bcrypt理论 60 字符,设VARBINARY(64);未来换算法也不用改表结构 - 别依赖文档写的“最大长度”,要实测:用你生产环境的参数生成 10 个哈希,取
max(len(h) for h in hashes)
MySQL 8.0+ 与 PostgreSQL 的 VARBINARY 行为差异
虽然都叫 VARBINARY,但两边对空字节(\x00)、尾部空格、比较逻辑处理不同,直接影响密码验证逻辑是否一致。
使用场景:
- 跨数据库迁移用户表(比如从 MySQL 迁到 PG)
- 应用同时连多个 DB 做灰度验证
关键差异点:
- MySQL 的
VARBINARY比较是逐字节,\x00和空格都参与;PostgreSQL 的BYTEA(等价物)默认也是二进制比较,但某些驱动会自动转义 - MySQL 8.0+ 支持
VARBINARY(N)最大到 65535,而老版本上限是 255;PostgreSQLBYTEA无硬限制,但超过 1GB 会走 TOAST - 如果用 ORM(如 SQLAlchemy、Django ORM),注意它可能把
VARBINARY映射成字符串类型,导致 Python 层解码失败 —— 必须显式声明字段类型为LargeBinary或bytea
ALTER TABLE 修改密码字段时最常踩的坑
线上表加字段容易,改已有字段类型却极容易锁表、丢数据、引发服务雪崩。
实操建议:
- 不要直接
ALTER TABLE users MODIFY password VARBINARY(64)—— MySQL 5.7 及以前会全表拷贝,锁表数小时 - 用
pt-online-schema-change(Percona Toolkit)或 MySQL 8.0+ 的ALGORITHM=INSTANT(仅限长度增大且不改类型) - 改之前先确认当前数据是否真超长:
SELECT MAX(LENGTH(password)) FROM users;;如果全是NULL或空字符串,说明还没写入过哈希,可以放心改 - 改完立刻验证:用一条已知密码的测试账号,注册 → 登录 → 查数据库确认字段内容和长度完全一致
真正麻烦的从来不是选 VARBINARY 还是 VARCHAR,而是字段一旦上线,改起来牵一发而动全身。长度设宽一点,比后期半夜紧急扩容安全得多。










