我们为何关注此事:我们是 Geonode,主要销售代理服务,因此经常看到包含凭据的命令——通常同一行中既包含代理密码,也包含目标密码。 首先需要强调的实际警告是:curl 命令中的凭据最终会出现在 shell 历史记录、进程列表中,以及您粘贴到支持工单中的任何内容里。 我们不止一次收到过包含真实密码的截图。下文介绍的 .netrc 方法只需约一分钟即可解决此问题,且完全免费,该方法同样适用于代理凭据,相关内容将在文末详细说明。
基本语法
curl -u username:password https://api.example.com/private
curl 手册 中对 -u, --user <user:password>
的说明如下:“指定用于服务器身份验证的用户名和密码。”
Basic 是默认的认证方案,因此 --basic
通常是多余的。手册中也明确指出:“使用 HTTP Basic 认证与远程主机通信。此方法是默认的,该选项通常没有意义,除非你用它来覆盖先前设置的、指定了其他认证方法的选项。”
实际在网络上传输的是一条请求头:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
即 username:password
的 Base64 编码形式——**是编码,而非加密。**任何能看到该请求的人都能一步解码。这就是为什么在明文 HTTP 上使用基本认证等同于以明文形式发送密码,因此它只应通过 HTTPS 使用。
手册中有一条语法限制:“用户名和密码以第一个冒号分隔,因此使用此选项时,用户名中不能包含冒号。但密码中仍可包含冒号。” 也就是说,密码中包含冒号没问题;用户名中包含冒号则不行。
为什么命令行并非合适之处
手册对此直言不讳:
在支持该功能的系统上,curl 会将给定的选项参数从进程列表中隐藏。但这并不足以防止凭据被同一系统上的其他用户看到,因为在凭据被清除之前,它们仍会短暂地显示出来。 此类敏感数据应从文件等来源获取,绝不能以明文形式出现在命令行中。
以下是四种独立的泄露途径,均属真实情况:
Shell 历史记录。 ~/.bash_history 或 zsh 的等效命令,以明文形式无限期保留。
进程列表。 在 curl 清除信息前的短暂时间内,该列表对机器上的其他用户可见。
日志。 任何记录脚本执行命令的内容。
粘贴的输出内容。 错误报告、问题跟踪系统、聊天消息、屏幕截图。
最后一种情况在实际中最为常见,却往往被忽视。
三种更安全的方法
1. 让 curl 提示输入。 只需提供用户名,curl 就会交互式地提示输入密码,并在读取时不显示密码:
curl -u username https://api.example.com/private
不会存储任何信息,也不会记录任何日志。对于任何手动输入的内容,这都是正确的方法。
**2. 使用.netrc
文件。** 手册中对-n, --netrc
的描述如下:“让curl扫描用户主目录中的.netrc文件以获取登录名和密码……若用于HTTP,curl将启用用户身份验证。”
创建 ~/.netrc
:
machine api.example.com
login myusername
password mypassword
随后需对其权限进行限制,因为 curl 不会自动处理——手册中指出“如果该文件没有正确的权限(既不应被所有人读取,也不应被组读取),curl 不会报错”:
chmod 600 ~/.netrc
curl -n https://api.example.com/private
手册中提到了三个有用的细节。“netrc 文件为主机名提供凭据,与所使用的协议和端口号无关”,因此一条记录即可覆盖一个主机。--netrc-file
“会覆盖所有其他用于确定该文件的方式”,这对于按项目管理的凭据文件非常方便。 此外,自 curl 8.16.0 起,可通过环境变量 NETRC
指定文件名。在 Windows 系统中,会同时检查主目录下的 .netrc
和 _netrc
,其中优先使用前者。
--netrc-optional
是一种变体,它会在文件存在时使用该文件,文件不存在时也不会报错——这更适合可能在两种状态下运行的脚本。
3. 从环境变量读取。 当使用文件不切实际时,至少应避免将其记录在历史记录中:
read -rs API_PASS
curl -u "myuser:${API_PASS}" https://api.example.com/private
请注意 read
中的 -s
,这样密码就不会被回显。但这在进程的环境中仍然可见,因此这只是折中方案,而非理想选择。
对于脚本,使用 .netrc
并配合 chmod 600
才是正解。这是手册中推荐的选项,也能彻底消除上述所有安全隐患。
Basic 与其他认证方案的对比
curl 支持多种认证方案,了解它们的区别可以避免一类常见的混淆。
| 选项 | 认证方案 | 传输中的密码 |
|---|---|---|
--basic | HTTP Basic(默认) | Base64 编码,实质上为明文 |
--digest | HTTP Digest | 哈希处理后的挑战-响应 |
--ntlm | NTLM | Windows 环境 |
--negotiate | SPNEGO / Kerberos | 基于凭证 |
--anyauth | 自动 | 取决于所选方案 |
--oauth2-bearer | Bearer 令牌 | 令牌本身 |
**摘要(Digest)**的文档说明如下:“启用 HTTP 摘要认证。此认证方案可避免将密码以明文形式通过网络传输。请将其与常规的 --user 选项结合使用,以设置用户名和密码。”
--anyauth 是一个便捷选项,但手册中明确指出了其代价: “自动确定身份验证方法,并使用远程站点声称支持的最安全方法。此过程首先会发送一个请求并检查响应头,因此可能会导致额外的网络往返。”
对于高并发场景,每次请求增加一次网络往返并非免费。此外,手册还特别警告了一种可能的故障:“若从标准输入(stdin)进行上传,不建议使用 --anyauth,因为这可能导致数据被发送两次,且客户端必须具备回卷能力。若在从标准输入上传时确实需要使用该方案,上传操作将失败。”
因此:当你确实不知道服务器要求什么时,请使用 --anyauth;一旦确定了方案,请明确指定。
Bearer 令牌是大多数现代 API 实际采用的方式,它们与基本认证完全不同:
curl --oauth2-bearer "mF_9.B5f-4.1JqM" https://api.example.com/me
这相当于手动设置 Authorization: Bearer ...。请注意,Bearer 令牌本身就是凭证——任何持有它的人都可以使用它——因此应像对待密码一样谨慎处理。
解读响应
身份验证失败时的简短排查路径。
**401 Unauthorized
** 表示服务器需要凭据,或者拒绝了您发送的凭据。响应中包含一个 WWW-Authenticate
头部,其中指明了服务器期望的认证方案,读取该头部即可避免盲目猜测:
curl -sS -o /dev/null -D - https://api.example.com/private | grep -i www-authenticate
如果显示 Digest
而您发送的是 Basic,那就是问题的答案。
**403 Forbidden
** 情况不同,且常被误读。这表示您已成功认证,但无权执行此操作。更改密码无济于事;调整权限或许有效。
**407 Proxy Authentication Required
** 表示 代理 需要凭据,而非目标服务器。这涉及不同的标头和不同的处理方式,将在下一节中说明。
**带有登录页面的 200
** 意味着该端点根本不使用 HTTP 身份验证——它使用表单和会话 Cookie,而 -u
对它毫无作用。检查 Content-Type
:如果你预期收到 JSON 却得到了 text/html
,那很可能就是这种情况。
要确认您实际发送的内容,请访问:
curl -v -u user:pass https://api.example.com/private 2>&1 | grep -i '^> authorization'
。请谨记手册中的警告:详细输出“可能包含敏感数据,包括用户名、凭据或机密数据内容”——在分享前请进行遮蔽处理。
自行构建请求头
有时 -u
生成的结果并非你所期望的,了解它实际生成的内容有助于你绕过其限制。
当用户名中包含冒号时。 -u
会按第一个冒号进行拆分,因此像 service:reader
这样的用户名无法通过该方式表示。请直接构建请求头:
CRED=$(printf '%s' 'service:reader:mypassword' | base64 -w0)
curl -H "Authorization: Basic ${CRED}" https://api.example.com/private
请注意使用 printf
而不是 echo
,后者会追加一个换行符,该换行符最终会出现在编码后的凭据中,从而导致令人费解的 401 错误。此外,请使用 base64 -w0
以防止换行——GNU base64
默认在 76 个字符处换行,而包含嵌入换行符的请求头会被视为格式错误的请求。在 macOS 上,直接使用 base64
不会换行,因此无需该标志。
当需要从密钥管理器获取凭据时。 大多数密钥管理工具会将输出写入标准输出(stdout),将值保存在变量中而非文件中可以限制其生命周期:
TOKEN=$(vault kv get -field=token secret/api)
curl -H "Authorization: Bearer ${TOKEN}" https://api.example.com/me
当您希望将标头写入配置文件而非命令行时。 curl 从 ~/.curlrc
读取选项,或从以 -K
命名的文件中读取:
# api-auth.conf
--user "myuser:mypassword"
--header "Accept: application/json"
curl -K api-auth.conf https://api.example.com/private
使用 chmod 600
限制文件。当 .netrc
不适用时(例如,当您需要令牌而非用户名和密码时),这是一种合理的折中方案。
关于 ~/.curlrc
的特别注意事项。 它适用于该用户的 所有 curl 调用,包括您未编写的那些。将凭据放置在此处,意味着会将其发送给任何脚本所请求的主机。对于任何敏感信息,请使用 -K
指定的命名文件;而 ~/.curlrc
则应保留用于无害的默认值,例如 --show-error
和 --location
。
代理身份验证是独立的
这是在我们的支持工单中引发最多困惑的一个区别。
可能涉及两组独立的凭据:一组用于代理,一组用于目标服务器。它们使用不同的请求头、不同的状态码以及不同的 curl 选项。
curl -x http://proxy.example.com:9000 \
--proxy-user proxyuser:proxypass \
-u apiuser:apipass \
https://api.example.com/private
和
--proxy-user
用于对代理进行身份验证;-u
用于对目标服务器进行身份验证。如果将它们混淆,就会出现本应返回 401 却返回 407 的情况,反之亦然。
代理方案也有相应的选项——--proxy-basic
、--proxy-digest
、--proxy-anyauth
、--proxy-negotiate
——与目标端选项相对应。
两点实用注意事项。
代理 URL 中的凭据存在同样的暴露问题,此外还有另一个问题。 -x http://user:pass@proxy:9000
会将密码放在命令行中,同时也会放在任何包含代理 URL 的环境变量中,而 http_proxy
通常就位于该环境变量中。 请对密码中的任何 @
、:
或 /
进行百分比编码,否则 URL 解析器会在错误的位置进行拆分。
请区分是哪一跳拒绝了你,而不是凭猜测:
curl -sS -o /dev/null -x "$PROXY" \
-w 'connect=%{http_connect} status=%{response_code}\n' \
https://api.example.com/private
connect=407
表示代理拒绝了你,且从未到达目标。connect=200 status=401
表示代理正常工作,但目标需要凭据。这两种情况的解决方法不同。
关于代理认证的特别说明:许多服务商提供 IP 白名单作为用户名和密码的替代方案。如果您的源地址是稳定的,这可以完全省去命令中的凭据,这是最干净的解决方案——无需文件、无需环境变量,没有任何泄露风险。
当网站根本不使用 HTTP 身份验证时
大量关于“curl 基本身份验证无法正常工作”的报告,其实是网站从一开始就从未使用过 HTTP 身份验证的情况。
如何判断。 尝试不带凭据访问受保护的 URL,并查看响应:
curl -sS -o /dev/null -D - https://example.com/dashboard
如果响应为 401
且包含 WWW-Authenticate
头部,则表示使用 HTTP 身份验证,此时应使用 -u
进行请求。若响应为 200
并跳转至登录页面,或响应为 302
并重定向至 /login
,则说明该网站使用表单和会话 Cookie —— 此时无论如何使用 -u
都无济于事,因为没有任何机制会读取该头部。
改用表单登录模式。 将凭据提交至登录端点,保留 Cookie 并重复使用:
curl -c jar.txt -d "username=ada&password=secret" \
https://example.com/login
curl -b jar.txt https://example.com/dashboard
-c
写入 Cookie,-b
读取 Cookie。如果服务器轮换会话 Cookie(许多服务器都会这样做),则在后续请求(-b jar.txt -c jar.txt
)中同时使用这两个 Cookie。
你会遇到的复杂情况:CSRF 令牌。 大多数登录表单都包含一个隐藏令牌,该令牌必须与凭据一起提交,并且是按会话生成的。这意味着需要一个两步流程——获取表单,提取令牌,然后将其与第一步获取的 Cookie 一起提交:
TOKEN=$(curl -sS -c jar.txt https://example.com/login \
| grep -o 'name="csrf_token" value="[^"]*"' \
| cut -d'"' -f4)
curl -b jar.txt -c jar.txt \
-d "csrf_token=${TOKEN}" -d "username=ada" -d "password=secret" \
https://example.com/login
对 HTML 进行 grep 和截取操作并不稳定,仅适用于一次性诊断。对于需要持续处理的情况,请先检查该服务是否提供支持令牌认证的 API——几乎所有服务都提供,这比维护一个用于抓取登录表单的爬虫要省事得多。
如果登录需要 JavaScript,curl 则完全无法处理。这并非 curl 的局限性而需要绕过;而是提示你寻找页面本身调用的 API 端点,你可以在浏览器的网络标签页中找到该端点并直接重现操作。
大家还问
如何在 curl 中使用基本认证?
curl -u username:password URL。Basic 是 curl 的默认认证方案,因此除非你需要覆盖之前设置的方法,否则 --basic 是多余的。请务必使用 HTTPS,因为基本认证对凭据进行的是 base64 编码,而非加密。
如何让 curl 提示输入密码?
只需提供用户名:curl -u username URL。curl 会交互式地请求密码,且不会回显密码,因此密码不会被记录到 shell 历史记录或进程列表中。对于任何手动输入的内容,这都是正确的做法。
curl 的基本认证安全吗?
仅在 HTTPS 环境下才安全。凭据经过 Base64 编码,这种编码方式极易被逆向解码,因此通过普通 HTTP 传输时,凭据实际上等同于明文。在 TLS 环境下,传输层会保护凭据,剩余的风险在于您在本地机器上存储凭据的方式。
如何将 curl 凭据存储到文件中?
使用 ~/.netrc,并在其中添加 machine、login 和 password 这几行,然后通过 chmod 600 设置权限,并使用 -n 调用 curl。curl 不会对错误的权限发出警告,因此设置权限是你的责任。--netrc-file 指向了另一个位置,而 --netrc-optional 可避免在文件不存在时出现错误。
为什么我的密码正确但 curl 却返回 401 错误?
有几种可能:服务器期望不同的协议方案——请检查 WWW-Authenticate 头部信息——或者该端点使用带 Cookie 的表单登录而非 HTTP 认证,又或者您的用户名中包含冒号,而 -u 无法正确处理该用户名,因为它会按第一个冒号进行拆分。
401 和 403 有什么区别?
401 表示您未通过身份验证:要么未提供凭据,要么凭据错误。403 表示您已成功通过身份验证,但无权执行此操作。对于前者,尝试使用不同的凭据可以解决问题;对于后者则无效。
如何使用 curl 对代理进行身份验证?
--proxy-user user:password,这与目标地址 -u 是分开的。 407 状态码表示代理服务器需要凭据;401 状态码表示目标服务器需要凭据。如果您的源地址是固定的,建议向服务提供商咨询 IP 白名单设置——这可以完全省去命令中的凭据。
--anyauth 选项的作用是什么?
它会让 curl 通过发送请求并读取响应头来检测服务器首选的认证方案,然后使用提供的最安全的方法进行认证。其代价是每个请求会增加一次往返通信,且手册中警告称,从标准输入 (stdin) 上传时可能会失败,因为数据可能需要发送两次。
总结
语法很简单:-u username:password,这样就能通过身份验证,其中 basic 是 curl 的默认认证方案。需要特别注意的是密码的存储位置。
curl 的官方手册对此直言不讳——凭证“应从文件等处读取,切勿在命令行中以明文形式使用”——而它所警告的安全风险都是真实存在的。 Shell 历史记录会无限期地保存这些信息,进程列表会短暂地将它们暴露给该机器上的任何人,而粘贴的终端输出则有着一种不可思议的能力,能流向你意想不到的地方。
养成两个习惯就能解决这个问题。在交互式使用时,只需提供用户名,让 curl 提示输入密码;在脚本中,将凭据存入 ~/.netrc 文件中,并使用 chmod 600 进行认证,同时使用 -n。这两种方法所花费的时间都不比直接输入密码更长。
同时要分清这两层身份验证。401 错误来自目标服务器,需要使用 -u;407 错误来自代理服务器,需要使用 --proxy-user。如果你的地址是固定的,白名单可以彻底从命令中移除第二组凭据——这才是存储密钥唯一真正安全的地方。
