htpasswd bcrypt 与 auth-api 格式不兼容导致登录全 401
遇现场景
tx 服务器(ubuntu@124.221.137.194)https://6767.chat/api/login 表单登录对任何密码都返回 401,包括正确密码。用户怀疑服务器被入侵、密码被改。同时 nginx 原生 auth_basic 弹窗(porta / kanna / agy / cc.6767.chat / mihomo.6767.chat)用同一密码却能登入。
发生条件
- nginx 原生
auth_basic_user_file与自研 Node 鉴权服务 共用同一个 htpasswd 文件/etc/nginx/.htpasswd-6767-admin。 - 该文件里的 hash 是 bcrypt(
$2y$05$) 格式(由htpasswd -B生成)。 - Node 端 auth-api 的
verifyAdminHtpasswdPassword()只认{SSHA}/{SHA}/{PLAIN}三种前缀,不认 bcrypt。
症状与误判
- 症状:
POST /api/login无论输什么密码都HTTP 401,但GET /api/login(登录页)返回 200,服务进程 PM2 状态 online,端口在监听。nginx Basic 站点用同一密码却 200。 误判 1:误以为"密码被改"或"服务器被入侵"。
- 反驳:bcrypt hash 用 Python
bcrypt.checkpw("Complicate0705{", hash)实测 匹配,密码Complicate0705{是对的,没被改。
- 反驳:bcrypt hash 用 Python
误判 2:误以为 home-api 服务挂了(CLAUDE.md 记 home-api 端口 3003,实测 3003 无监听)。
- 反驳:home-api 实际监听 3017(
ss -tlnp确认);/api/login根本不归 home-api,nginx 配置里location = /api/login → proxy_pass 127.0.0.1:3021/monitor/login,3021 才是 auth-api。CLAUDE.md 端口/职责记录过时。
- 反驳:home-api 实际监听 3017(
尝试与排除
ssh tx连通正常,hostnameVM-0-5-ubuntu,排除机器离线。pm2 list全 online,排除进程挂掉。curl 127.0.0.1:3003→HTTP 000(端口无监听)→ 但这是探错端口,不是服务故障。curl https://6767.chat/api/loginGET → 200(登录页正常);POST 错误密码 → 401;POST 正确密码Complicate0705{→ 同样 401。任何密码都 401 = 服务端校验逻辑根本认不出存的那串 hash。- 读 nginx 配置确认
/api/login → 3021(auth-api),不是 home-api。 - 读 auth-api 源码,定位
verifyAdminHtpasswdPassword()的前缀分支:只{SSHA}/{SHA}/{PLAIN},无 bcrypt 分支。
根因
/etc/nginx/.htpasswd-6767-admin 在 2026-08-12 20:50 被重写为 bcrypt($2y$05$):
admin:$2y$05$iISckglMzlWq05HqahfpU.r1HYQZTrg58rrblSCyhQXrGhNdugUS6- nginx 原生
auth_basic支持 bcrypt → Basic 站点正常。 - auth-api(Node) 不支持 bcrypt →
/api/login对任何密码return false→ 401。
即:两个消费者对同一口令文件的格式支持集不一致,选了交集之外的格式,Node 侧静默失败。 不是密码错,不是被入侵,是一次配置事故。背景概念见 密码哈希的盐与彩虹表 (Salt & Rainbow Table)。
解决步骤
采用最小改动方案:把 hash 改成 两边都认的 {SHA} 格式(nginx 与 auth-api 均原生支持),不装依赖、不改代码、不重启服务。
- 备份:
sudo cp /etc/nginx/.htpasswd-6767-admin /etc/nginx/.htpasswd-6767-admin.bak.<ts>.bcrypt 用 openssl 现场算
{SHA}(密码经 stdin/管道喂入,避免进 shell 历史):SHA=$(printf "%s" "Complicate0705{" | openssl sha1 -binary | base64) echo "admin:{SHA}$SHA" | sudo tee /etc/nginx/.htpasswd-6767-admin > /dev/null- 复权:
sudo chown ubuntu:www-data /etc/nginx/.htpasswd-6767-admin; sudo chmod 640 四层验证(见远端 CLAUDE.md 测试四层结构):
POST /api/login正确密码 →HTTP 302 → /monitor✅- nginx Basic 错误密码 → 401,正确密码 → 200 ✅(cc / porta)
- 本地
printf ... | openssl sha1 -binary | base64自校验 hash 一致 ✅
- 无需
nginx -s reload、无需pm2 restart(htpasswd 每请求重读)。
遗留权衡:{SHA} 是无盐 SHA-1,抗彩虹表/穷举弱于 bcrypt。短期可接受(仅挡管理后台、有 HTTPS、有其他防护);长期建议给 auth-api 的 verifyAdminHtpasswdPassword 加 bcrypt 分支或改用 {SSHA}(带盐,代码已支持验签)。概念区别见 密码哈希的盐与彩虹表 (Salt & Rainbow Table) 第 3、5 节。
下次怎么快速识别
特征三联征,命中其一即可怀疑本坑:
- 分岔症状:同一 htpasswd 文件,nginx Basic 弹窗能登,但表单/自研 API 登录对任何密码都 401(或反之)。
- "任何密码都 401":不是某条密码错,而是校验函数对存的那种 hash 格式根本不认,直接走
return false。 - hash 前缀与代码分支对不上:
cat .htpasswd看前缀($2y$/$apr1$/{SHA}/{SSHA}),再grep校验函数认哪些前缀,取交集。
诊断命令链(按顺序):
ssh tx 'sudo cat /etc/nginx/.htpasswd-6767-admin' # 看前缀
ssh tx 'grep -n "startsWith\|verify" .../auth-api/server.js' # 看认哪些前缀
ssh tx 'curl -sk -X POST https://6767.chat/api/login -d "{\"username\":\"admin\",\"password\":\"X\"}" -w "%{http_code}"' # 看是否"任何密码都 401"相关概念
- 密码哈希的盐与彩虹表 (Salt & Rainbow Table) —— 盐、彩虹表、cost、bcrypt vs SHA-1 的体系化背景。
- Htpasswd 基础鉴权 —— htpasswd 工具命令、哈希算法选型(
-Bvs-s)、文件格式。 - Web 登录鉴权 (Session, Cookie, Bearer, JWT) —— Basic Auth 在整个鉴权体系里的位置与局限。
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web