密码哈希的核心矛盾在于一个不对称目标:正常登录时校验速度越快越好,攻击者拿到哈希后暴力破解的速度却越慢越好。md5与bcrypt在密码存储场景中的差距,本质上是固定低成本摘要算法与可调节成本密码哈希算法之间的差距。

PostgreSQL数据库提供多种哈希能力,其中md5函数可以非常方便地生成128位摘要,但也正是这种方便让不少系统长期陷入高风险状态。下面从算法差异、数据库实现和生产迁移三个角度展开。
一、md5与bcrypt的算法本质差异
md5的设计目标是快速生成报文摘要,通常用于数据完整性校验。它对任意长度输入输出固定32位十六进制字符串,计算过程不包含盐,也不包含任何可调成本参数。单次md5的计算耗时在微秒级别,现代GPU每秒可以完成数十亿次md5运算。攻击者在拿到数据库中的md5密码哈希后,不仅可以使用彩虹表直接查询常见密码,还能以极低成本遍历大量候选口令。
加盐通常被认为可以缓解md5的彩虹表问题,例如把固定盐或随机盐拼接在密码后再计算md5。但盐只能让相同的密码产生不同摘要,无法改变md5计算速度极快的事实。如果盐与哈希一起泄露,攻击者仍然能够以同样的高速度对每个盐分别暴力破解。更严重的是,很多系统使用固定盐甚至无盐,导致同一密码在不同账户中摘要完全一致,攻击者一眼就能识别重复弱密码。
bcrypt则基于Blowfish密钥调度算法,设计目标正好相反。它内部自动生成128位随机盐,并通过成本因子控制迭代次数,实际计算量是2的cost次方。bcrypt输出格式包含版本、成本因子、盐和最终哈希,常见的哈希串以$2a$或$2b$开头,长度约为60个字符。成本因子从4到31可调,每增加1,计算量翻倍。将cost设为10时,CPU校验一次通常需要几十毫秒,而cost设为12时可能达到数百毫秒。对普通用户来说只是登录多等一瞬间,但对批量暴力破解来说是指数级的时间成本增长。
二、PostgreSQL中如何使用bcrypt与md5
PostgreSQL原生提供md5函数,调用方式非常简单。开发人员常在用户表上存储字符串类型哈希,并通过一条SQL完成密码比对。下面示例先创建一个用户表,再插入md5哈希。
CREATE TABLE users (
username text PRIMARY KEY,
password_hash text NOT NULL
);
INSERT INTO users (username, password_hash)
VALUES ('alice', md5('mypassword'));
这条SQL执行后,alice的password_hash列保存的是无盐md5摘要。任何拿到数据库读取权限的人都可以直接比较该摘要与常见密码字典。若要在PostgreSQL中生成bcrypt哈希,推荐使用pgcrypto扩展。它提供了crypt函数和gen_salt函数,bcrypt对应的盐类型标识为bf。启用扩展和生成哈希的SQL如下。
CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (username, password_hash)
VALUES ('bob', crypt('mypassword', gen_salt('bf', 12)));
这里gen_salt的第二个参数12表示成本因子,即bcrypt会执行2的12次方轮密钥调度。验证密码时,不需要重新生成盐,直接把用户输入的明文和库中已有哈希一起传给crypt函数,PostgreSQL会从已有哈希中解析出盐和成本参数,再按照相同参数计算并比对。验证SQL如下。
SELECT (password_hash = crypt('输入密码', password_hash)) AS is_valid
FROM users
WHERE username = 'bob';
bcrypt哈希长度固定为60个字符,因此password_hash列不需要太大,使用text或varchar(60)均可。需要注意,数据库中的md5认证协议与这里讨论的密码存储哈希不是同一概念。PostgreSQL在早期版本支持md5认证方式,用于客户端连接认证,但该方式并不涉及bcrypt,也存在协议层风险。当前版本推荐使用scram-sha-256作为连接认证方法,而应用层存储用户的登录密码时再用bcrypt哈希。两者分别解决不同问题,不应混用。
三、从md5迁移到bcrypt的详细步骤
存量系统最常见的状态是用户表中已经有大量md5哈希,不可能要求所有用户立即重新注册。比较稳妥的方式是惰性迁移,即用户下次成功登录时,用正确的明文密码重新生成bcrypt哈希并覆盖旧值。由于bcrypt哈希以$2开头,md5是32位十六进制字符串,可以通过格式判断当前用户处于哪种哈希版本。
下面创建一个可同时兼容旧md5和新bcrypt的验证函数。它先读取当前哈希,如果已经是bcrypt则直接校验;如果仍然是md5,则先用md5验证,验证通过后立刻升级为bcrypt。这样用户登录一次后,数据库中的哈希就完成了迁移。
CREATE OR REPLACE FUNCTION verify_and_upgrade_password(
p_username text,
p_password text
) RETURNS boolean AS $$
DECLARE
v_hash text;
BEGIN
SELECT password_hash INTO v_hash
FROM users
WHERE username = p_username;
IF v_hash IS NULL THEN
RETURN false;
END IF;
IF v_hash LIKE '$2%' THEN
RETURN v_hash = crypt(p_password, v_hash);
END IF;
IF v_hash = md5(p_password) THEN
UPDATE users
SET password_hash = crypt(p_password, gen_salt('bf', 12))
WHERE username = p_username;
RETURN true;
END IF;
RETURN false;
END;
$$ LANGUAGE plpgsql;
调用该函数时,如果旧用户密码正确,函数返回true,同时这一行数据中的md5哈希会被替换为bcrypt哈希。未登录的旧用户不会受到打扰,但他们的md5哈希仍然存在风险。因此迁移方案只能作为过渡手段,不能作为长期兼容策略。建议在上线一段时间后统计仍为md5的账户数量,通过邮件、短信或强制下线方式要求这些用户重置密码。
迁移过程中还要考虑旧md5哈希是否有盐。如果原系统使用的是加盐md5,旧验证逻辑需要保持与原来一致的盐计算方式,否则升级判断会失败。对于已泄露过的数据库,迁移到bcrypt之前最好先让所有用户重置密码,因为旧md5可能已经被攻击者破解。迁移完成后,应删除代码中所有md5验证逻辑,避免任何路径回退到弱哈希。
四、成本因子选择与数据库性能影响
bcrypt的成本因子越大,单次校验耗时越长,数据库在高并发登录场景下的CPU消耗也越高。生产环境需要根据业务特征选择合适的cost值。一般建议先使用cost为10或11进行压测,观察登录接口的平均延迟和数据库CPU使用率。对于安全性要求高的系统,可以把cost设为12;对于登录极其频繁但账号价值较低的场景,可以暂时使用10,但要避免低于10。
除了使用pgcrypto在数据库内计算bcrypt,也可以把bcrypt计算放在应用层。应用层bcrypt库通常更容易横向扩展,也能避免数据库拖库后攻击者直接利用数据库CPU去做大量哈希计算。但无论在哪一层计算,数据库中保存的都应该只有bcrypt结果,绝不能保存明文或可逆加密的密码。即使应用层已经做了bcrypt,PostgreSQL中的列依然要设置合适长度,并限制数据库账户权限,最小化哈希泄露面。
最后要明确,密码哈希算法的选择不能只停留在更换函数名称。bcrypt之所以安全,是因为它把盐和成本控制内建在算法中,并且输出格式自描述,验证端不需要额外保存盐字段。md5即便配合加盐和多次迭代,也无法达到bcrypt同等的抗暴力破解能力,因为md5本身设计不考虑密码学缓慢计算。PostgreSQL通过pgcrypto扩展可以低成本获得bcrypt能力,迁移路径清晰,是替换md5的合理选择。
PostgreSQL密码哈希bcryptmd5修改时间:2026-08-26 13:52:10