花拾录
← 返回知识库

连接串密码里有 @ 和 !,客户端把后半截当成了主机名

数据库导入2026/09/220 阅读0 评论

同一个数据库,用命令行工具连得好好的,用应用代码连就报"主机名解析失败"。密码是对的,主机也是对的,但客户端把密码的一部分当成了主机名。

现象

从某个应用用连接串连接 PostgreSQL 时报错,连不上,错误信息指向一个奇怪的主机名——那串东西你根本没在配置里写过。

而另一个客户端用"看起来同样的"连接信息却能正常连接。于是问题被误判成"这个应用的驱动有问题",甚至有人开始考虑换驱动。

could not translate host name "ssw0rd!@dbhost" to address

如果你仔细看这个主机名,会发现它是密码的后半截拼上真正的主机名——这就是线索。

根因

密码里含有特殊字符,而连接串没有做 URL 编码。

连接串的格式大致是:

postgresql://用户名:密码@主机:端口/库名

解析逻辑是:从 :// 之后开始,第一个 : 之前是用户名,: 之后到第一个 @ 之前是密码,@ 之后是主机部分。

如果你的密码里含 @,比如 p@ssw0rd!,那么:

  • 用户名 = appuser
  • 密码 = p
  • 主机 = ssw0rd!@dbhost:5432(错误!)

客户端把第一个 @ 当作"用户信息与主机信息的分隔符",于是密码被截断,剩下的内容被当成主机名。报错说"解析出错误的主机名"就是这么来的。

另一个客户端之所以"正常",是因为某些驱动对连接串的解析更宽容(或者接受别的格式),恰好没触发这个问题——这正是最误导人的地方:"另一个能连,说明配置没错。"

顺便说明:! 本身不是关键,关键是那些在连接串里有语法含义的字符。

解决

连接串里所有密码特殊字符做 URL 编码(percent-encoding):

@  → %40
!  → %21
:  → %3A
/  → %2F
?  → %3F
#  → %23
%  → %25
&  → %26

所以密码 p@ssw0rd! 应该写成 p%40ssw0rd%21:

postgresql://appuser:p%40ssw0rd%21@dbhost:5432/mydb

规矩就是:密码里出现 @ : / ? # % & 一律编码。

在代码里可以用工具函数做,别手写:

from urllib.parse import quote
user_pw = quote("p@ssw0rd!", safe="")
url = f"postgresql://{user}:{user_pw}@{host}:{port}/{dbname}"

注意 safe=""——它表示"连 / 也一并编码",避免留下任何有可能被当成分隔符的字符。

反过来也要注意不要编码过头:编码只作用于密码(以及用户名)部分,主机名、端口、库名不需要编码——它们本身就不该包含这些特殊字符。把所有部分一律编码,反而会把 : 和 / 这些结构分隔符也改写掉,让连接串彻底失效。

延伸与预防

第一条:两个客户端都要各自验证一遍。 不要因为"命令行能连"就认为应用也能连——不同的驱动对连接串的解析实现不一样,宽容度也不一样。凡是涉及连接串的地方,用实际的那个客户端跑一次。

第二条:能拆开传就拆开传。 比如 psycopg 支持分开传参:

conn = psycopg.connect(
    host="dbhost", port=5432, dbname="mydb",
    user="appuser", password="p@ssw0rd!"   # 原样传,不用编码
)

这样密码不需要编码,从根本上避开了解析歧义。绝大多数驱动都支持这种参数化写法,优先用它。

第三条:如果密码是随机生成的(很常见,尤其是自动部署场景),一定要检查里面有没有这些特殊字符。随机生成器可不会避开 @ 和 :。可以在生成密码时就限制字符集,或者生成后立即做一次连接测试。

最后补一条:改密码时也要同步检查连接串。轮换密码是一件独立于"改密码"的动作——如果新密码里带了特殊字符而连接串没编码,应用会在下一次重连时突然失败,而密码本身其实是对的。把它们放进同一个变更流程里,能避免这种"半夜挂掉"的事故。

再补一个几秒钟定位的技巧:把报错里的"主机名"和你配置里的密码原样对照一下。如果那个主机名明显是密码的后半截,就基本可以确诊是 URL 编码问题,而不是 DNS 或网络问题——这个动作能立刻把方向从"查 DNS"扭到"检查连接串编码"。

评论(0)

  • 还没有评论,来抢沙发~

相关文章