安全指南

HTTP Basic 认证与 htpasswd:它是怎么工作的,什么时候该用

Basic 认证在链路上究竟发了什么字节、为什么离开 TLS 就毫无意义、htpasswd 各种哈希算法各值多少、bcrypt 成本该怎么选,以及 Basic 认证在哪些场景才是正确答案。

HTTP Basic 认证是 Web 上最古老、也最没有神秘感的认证方案:客户端把用户名和密码做 Base64 编码,随每一个请求发送。这个设计要么正是你想要的,要么完全不可接受,区别完全取决于你在保护什么。

链路上实际发生了什么

RFC 7617 规定的这套交互只有三步,且没有状态:客户端请求一个受保护资源;服务器返回 401 Unauthorized,附带写明方案和 realm 的 WWW-Authenticate 头;客户端随后带上 Authorization 头重试,内容是单词 Basic 加一个 Base64 串。

那个串就是「用户名 + 冒号 + 密码」的 Base64 编码,没有别的。没有哈希,没有 nonce,没有质询应答,也没有密钥协商。这也是用户名里不能含冒号的原因:服务端按第一个冒号切分,之后的冒号都归密码。

两条推论立刻成立。第一,任何能读到这个头的人都能还原出凭证——Base64 是编码,而编码的意义就在于可逆。第二,完整密码随每个请求传输,而不是只在登录时传一次。这里没有会话可偷,因为根本没有会话;密码本身就是会话令牌,被无限次重放。

一次完整的 Basic 认证交互
GET /metrics HTTP/1.1
Host: internal.example.com

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="Internal metrics", charset="UTF-8"

GET /metrics HTTP/1.1
Host: internal.example.com
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l

# 这个串解码出来正是下面这行,全程不涉及任何密钥:
$ echo 'YWxhZGRpbjpvcGVuc2VzYW1l' | base64 -d
aladdin:opensesame

# charset 参数只允许取 "UTF-8",它告诉客户端在做 Base64
# 之前该如何编码非 ASCII 凭证。

为什么必须有 TLS,以及 TLS 修不好什么

在明文 HTTP 上使用 Basic 认证,等于把一个可重复使用的密码以可读形式发给链路上的每一台设备。这不是一个需要与便利性权衡的微妙弱点,而是一次多绕了几步的密码泄露。如果某个服务使用 Basic 认证,它必须只走 HTTPS,并且要么在弹出凭证提示之前就把 HTTP 重定向掉,要么直接拒绝。

TLS 彻底解决了传输问题,同时原封不动地留下另外三个——而这部分往往被略过。

凭证在每个请求上重放,于是任何看得见请求的东西都看得见密码:配置失误导致记录请求头的访问日志、异常时采集请求头的错误上报系统、终结 TLS 的中间代理,以及浏览器自己的开发者工具和导出的 HAR 文件。持有型令牌也有同样的性质,但它至少有作用域、可以吊销;Basic 密码通常两样都没有。

htpasswd 文件及其算法

Basic 认证的服务端通常就是一个纯文本文件:每行一个用户,用户名和密码哈希以冒号分隔,哈希前缀标明算法,风格与 Unix shadow 条目一样自描述。

算法的选择比看上去重要得多,因为这个文件就是一个密码库。它往往会流落到配置仓库、容器镜像层、备份,或者配置管理系统的清单里——在这些地方,弱哈希对攻击者的价值远高于强哈希。请使用 bcrypt,并且无论如何都把这个文件当作机密对待。

有一处容易混淆的地方值得澄清:Apache 的 -m 参数名字叫 MD5,但它并不是裸的 MD5 摘要,而是 APR1 算法——它对密码加盐并迭代一千次 MD5。这在 1996 年是合理设计,也远好于无盐摘要,但一千轮 MD5 在现代硬件上快到可以忽略。出于兼容原因,它在许多平台上至今仍是默认值,也就是说:接受默认,你拿到的就是弱选项。

参数算法存储前缀是否加盐结论
-Bbcrypt,成本由 -C 指定$2y$是,128 位用这个。Apache 自 2.4.4 起支持。
-mAPR1:加盐 MD5,迭代 1000 次$apr1$是,48 位历史默认值。离线破解很快——请替换掉。
-2 / -5SHA-256 / SHA-512 crypt,带迭代$5$ / $6$在没有 bcrypt 时可以接受。仅较新的 Apache 构建支持。
-sSHA-1,单轮{SHA}无盐,破解是瞬时的。不要使用。
-dcrypt(3) DES无前缀,13 个字符是,12 位会把密码截断到 8 个字符。仅限历史遗留。
-p明文原样存储密码。仅 Windows 与 NetWare 支持,并且永远不要用。
各格式的真实 htpasswd 输出
$ htpasswd -nbB -C 10 deploy 'correct horse battery staple'
deploy:$2y$10$BGmm0BZWcCbLgebJ4eSabOpuY0/mHQl/iSB6wNIqSFwTnVn9J1cDW

$ htpasswd -nbm legacy 'hunter2'
legacy:$apr1$8LOMaYO6$fg2450HshNgRkR2i0sfcx.

$ htpasswd -nbs legacy 'hunter2'
legacy:{SHA}87u9ZqY9S/F0eUBXjsPQEDUw4h0=

# -n 表示输出到标准输出而不写文件;-b 表示密码作为参数传入,
# 这会把它留在 shell 历史里。去掉 -b 就会改为交互式输入。
#
# {SHA} 那一行没有盐:同一个密码在全世界任何机器上都会
# 产生同一个字符串,所以它是可以被「查表」而不是被破解的。

怎么选 bcrypt 成本

-C 参数设置 bcrypt 的成本因子,它是唯一决定「离线攻击你这个文件要花多少代价」的参数。每加一档工作量翻倍:成本 12 是成本 10 的四倍,是成本 5 的一百二十八倍。

Apache 的默认值是 5,这对 2026 年来说太低了,也是真实 htpasswd 文件里最常见的单一缺陷。这个默认值定于 bcrypt 刚问世、硬件还慢的年代,此后从未上调。只要你敲 htpasswd -B 而不带 -C,拿到的就是它。

选成本要靠实测而不是靠传说,因为正确答案取决于你的硬件。在当前一代笔记本的单核上,成本 10 每次验证约 40 毫秒,成本 12 约 170 毫秒。请选择「每个请求都能承受其延迟」的最高值——记住 Basic 认证是每请求验证一次,而不是每会话一次,所以在高流量端点上用成本 14 就是自己给自己制造拒绝服务。对一道 Basic 认证闸门来说,10 到 12 是合理区间,并且应该在真正对外服务的那台机器上做基准测试。

Apache 的 htpasswd 接受 4 到 17 之间的成本值。还要注意 bcrypt 的 72 字节输入上限:超出部分会被静默截断,所以特别长的口令短语在那之后不再带来任何增益。

在将要运行它的机器上实测成本
$ time htpasswd -nbB -C 10 u p > /dev/null
real  0m0.04s

$ time htpasswd -nbB -C 12 u p > /dev/null
real  0m0.17s

# 每加两档约四倍,符合预期。选出「每个请求都付得起」的
# 最高成本,然后把它写进运维手册,免得下一个人又悄悄
# 退回到默认的 5。

接到服务器上

在 nginx 上,配置就是 location 或 server 块里的两条指令;再嵌一个带 auth_basic off 的 location 就能豁免某个路径——这对「负载均衡器必须免凭证访问的健康检查」很有用。

有一个平台细节会绊住人。nginx 自己验证 APR1 和 {SHA} 条目,但把 DES 以及现代 crypt 格式交给系统的 crypt(3)。因此 bcrypt 条目能不能用,取决于 C 库:基于 libxcrypt 构建的发行版能正确处理 $2y$,而只有旧版 glibc 的系统不行。如果同一条 bcrypt 记录在一台主机上被拒、在另一台上被接受,原因就在这里。上线前请在真正的目标机器上测一下这个文件。

nginx 与 Apache 配置
# nginx
location /internal/ {
    auth_basic           "Internal metrics";
    auth_basic_user_file /etc/nginx/secrets/htpasswd;

    location /internal/healthz {
        auth_basic off;
    }
}

# Apache
<Directory "/srv/app/internal">
    AuthType Basic
    AuthName "Internal metrics"
    AuthUserFile /etc/apache2/secrets/htpasswd
    Require valid-user
</Directory>

# 把文件放在任何对外目录之外,权限 0640,属主 root,
# 只让服务器运行用户可读。

什么时候该用,什么时候不该用

Basic 认证确实擅长一件事:在「本来就不该公开可达」的东西前面,立一道廉价、无状态、无依赖的闸门。不希望被收录和被扫描的预发布环境。Prometheus 指标端点。已经在 VPN 后面、但你还想再加一层的内部看板。两端都由你掌控、可以轮换长随机密码的 webhook 接收端。在这些场景里,它在代理层就能决策,不需要应用代码,能在服务被重写之后继续存在,也没有需要持续打补丁的库。

对任何有真实用户的东西,它都是错误答案。没有登出、没有密码重置、没有第二因素、失败多次不会锁定、没有按设备的会话,也没有办法在不改文件并重载服务器的前提下吊销某一个人的访问权。浏览器弹窗无法定制样式、无法解释上下文,用户看到的只是一个写着 realm 字符串的裸对话框。同一主机同一 realm 下的多个账号之间还会相互干扰。这里每一条既是安全问题,也是产品问题。

最有力的说法是:用 Basic 认证把陌生人挡在「本不该公开的东西」之外,但永远不要用它来区分两个合法用户。如果「现在登录的是谁」这个答案对你的应用有意义,你需要的是真正的认证。

在浏览器里生成 htpasswd 记录

本站的 htpasswd 生成器在页面内产出一条 bcrypt 记录,成本因子由你选择,不向服务器发送任何内容。在没装 Apache 工具时这确实有用——精简容器和 Windows 工作机上常常没有它。

值得把它改变了什么、没改变什么说准确。输出的那一行是哈希,公开一条成本 10 的 bcrypt 哈希并不是灾难。真正敏感的是你为生成它而键入的那个密码,而它待在过一个浏览器文本框里:拿到主机权限的扩展够得着它,页面内存里有它,而且离「某次自动填充意外把它存到你没打算存的地方」只差一步。所以请专门为此生成一个全新的随机密码,不要复用任何还在保护别的东西的密码,并且用你的密码管理器来生成它。

本机有工具的时候优先用本机的。htpasswd -B -C 12 -n user 会以交互提示读取密码,而不是从文本框读,也不会把它留在 shell 历史里,全程不涉及浏览器。这里的生成器是在没有那个条件时的便利替代,而不是对它的升级——我们宁愿把这句话说出来,也不想让你误会。

要点回顾

  • Basic 认证只是把「用户名:密码」做 Base64 并在每个请求上重放,因此只有在 HTTPS 之上才勉强可以接受。
  • TLS 保护的是传输,保护不了你的日志、错误上报系统、终结 TLS 的代理,以及内嵌凭证的 URL。
  • htpasswd 请用 bcrypt 并显式指定成本——默认的 5 太低了,今天的合理区间是 10 到 12。
  • 上线前确认服务器真的能读 bcrypt 条目;nginx 把它交给系统 crypt(3),而各平台的支持情况不同。
  • 用 Basic 认证把陌生人挡在预发布站点或指标端点之外,绝不要拿它当真实用户的登录——那需要登出、重置、多因素和吊销能力。

继续了解相关检查与工具