前言
最近用 Next.js 写了一些前后端项目,其中使用了 better-auth 做用户认证。
不过实践中发现 AI 直接写相关的代码很容易就明文传输密码了,虽然线上正常都会走 HTTPS,传输层已经有 TLS 加密,但这并不意味着就可以随便把密码明文塞进请求体里。
HTTPS 只能保证浏览器到 TLS 终止点(如 nginx)之间的传输安全,密码在反代、网关、日志、数据库等仍然以明文形式出现,这意味着任何人只要能拿到这些访问权限,就能顺利登录所有人的账号,个人项目完全可以忽略密码加密,但显然不是一个产品能接受的。
因此有了这篇博客,讲解如何使用 better-auth 时在应用层对密码进行 RSA 非对称加密,或许将语料为给 AI 也能在其他项目中实现类似的,但请遵守 MIT 协议进行署名,详见文尾。
整体流程
- 用户在浏览器输入密码。
- 前端读取
NEXT_PUBLIC_RSA_PUBLIC_KEY。 - 使用 RSA-OAEP 对密码加密。
- 加密包里附带时间戳、随机盐、请求 ID、HMAC 签名。
- 前端把加密后的字符串传给 better-auth。
- 服务端在 better-auth 的
password.hash/password.verify中解密。 - 最终数据库里只保存 bcrypt hash。
整个流程中,明文密码仅出现在用户输入密码时。
客户端加密
这里没有只加密密码本身,而是把几个字段一起放进了 RSA 密文里:
password|timestamp|salt|requestId
其中:
password是用户输入的原始密码。timestamp用于判断请求是否过期。salt让相同密码每次生成不同密文。requestId用于防重放攻击。
登录页面中,提交给 better-auth 前会先调用 rsaEncrypt。
注册时也是一样:
这样抓包时看到的 password 字段已经不是原始密码,而是一段 RSA 加密后的数据包。

服务端解密
首先用私钥解密:
然后统一做校验:
这里主要做了几件事:
- 检查加密包格式。
- 检查时间戳是否过期。
- 检查
requestId是否已经用过。 - 用 RSA 私钥解密。
- 检查外层时间戳和内层时间戳是否一致。
- 检查外层请求 ID 和内层请求 ID 是否一致。
- 校验 HMAC 签名。
- 返回密码的 bcrypt hash。
requestId 的去重使用 Redis:
也就是只有第一次请求能写入成功,后续重复使用同一个 requestId 会被拒绝,防重放。
公钥私钥对的生成命令如下:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private_key.pem
openssl rsa -in private_key.pem -pubout -out public_key.pem
当然直接用ssh-keygen也是可行的,以及务必需要保证私钥的安全。
接入 better-auth
better-auth 默认会处理邮箱密码登录,不过这里需要自定义密码处理逻辑以实现加密和哈希。
这里有一个细节:better-auth 的 minPasswordLength 被设置成了 1,maxPasswordLength 设置成了 4096。
原因是传给 better-auth 的已经不是原始密码了,而是 RSA 加密后的数据包。如果继续用默认长度限制,很容易在 better-auth 层就被拦截。
如果需要限制密码长度,只能放在客户端 rsaEncrypt 中进行。
当然,better-auth 默认只有在登录和注册的场景下会需要验证密码,诸如修改密码、注销账户之类的地方,只要复用 rsaEncrypt 和 rsaDecryptAndValidate 即可。
better-auth 其他安全配置
除了密码加密,还可以在初始化 betterAuth 时配置了一些安全项:
含义大致如下:
- 不关闭 CSRF 检查。
- 生产环境使用 Secure Cookie。
- Cookie 设置
httpOnly。 - Cookie 设置
sameSite: "lax"。
以及登录、注册、找回密码的限流:
这样可以减少撞库、刷注册、刷邮件等问题。