📘 第六章 HTTP首部
📄 一句话概括

Proxy-Authenticate 只出现在 407 里,作用是告诉客户端「用哪种方案、 对哪个 realm 来认证我」。少了它,客户端只知道被拒,不知道该怎么补。

📝 本节和前面几节的关系

上一节的 Location 是给客户端指路,这一节是代理在门口拦人。 它和 6.4.15 的 Proxy-Authorization 是一对:一个要,一个给。

一、407 必须带它

401 和 407 都是「要认证」,区别是拦人的是谁:401 是目标服务器,407 是中间的代理。 相应地,401 用 WWW-Authenticate,407 用 Proxy-Authenticate。 Demo 里跑了两组 407:不带这一行的那组,响应只有 Proxy-Connection 和 Content-Length: 2; 带的那组多出 Proxy-Authenticate: Basic realm="gw"。 差别只有一行,后果差很远。客户端要发 Proxy-Authorization,得先知道方案名(Basic?Digest? )和 realm,否则拼不出来。缺了这一行,它就只能干瞪眼——这正是很多自研代理「认证总也过不去」的原因。

二、两套凭据各走各的路

Proxy-Authorization 是发给代理的,Authorization 是发给目标服务器的。 关键在于:代理处理完 Proxy-Authorization 之后必须把它删掉再转发,否则凭据就漏给了源站。 D082 那组实测过这件事:客户端发了 9 个首部,源服务器只收到 3 个, Proxy-Authorization 就在被丢掉的那 6 个里。 反过来,Authorization 要一路带到源站,代理不能动它。 这两个字段名字只差一个前缀,转发规则正好相反,很容易写错。

三、realm 只是个标签

realm="gw" 里的 gw 是「保护区名字」,纯粹给人看——浏览器弹登录框时会显示它。 它不参与任何加密或校验。 所以别指望 realm 能隔离权限。真正的隔离靠的是每一跳独立校验凭据,而不是靠名字取得好听。 还有一点:Basic 方案只是把账号密码 base64 一下,等于明文。 代理认证如果要过公网,必须跑在 TLS 里,否则同网段抓包就能看到密码。

四、动手 Demo:同一台代理,带不带那一行的差别

同一个 407 处理器跑两遍,唯一的开关是「响应里要不要塞 Proxy-Authenticate」, 看客户端能不能自己拼出凭据。

def challenge(with_hdr):
    def fn(raw, st):
        has = any(ln.lower().startswith(u'proxy-authorization:')
                  for ln in X.head_lines(raw))
        if not has:
            hd = [(u'Proxy-Connection', u'close')]
            if with_hdr:
                hd.insert(0, (u'Proxy-Authenticate', u'Basic realm="gw"'))
            return X.reply(u'HTTP/1.1 407 Proxy Authentication Required', hd, b'no')
        return X.reply(u'HTTP/1.1 200 OK', [], TEXT)
    return fn

实跑输出:

--------------------------------------------------------------------
Proxy-Authenticate:告诉客户端「拿什么来找我认证」
--------------------------------------------------------------------
  407 带 Proxy-Authenticate:False
    HTTP/1.1 407 Proxy Authentication Required
    Proxy-Connection: close
    Content-Length: 2
    Connection: close
    服务端被访问 1 次

  407 带 Proxy-Authenticate:True
    HTTP/1.1 407 Proxy Authentication Required
    Proxy-Authenticate: Basic realm="gw"
    Proxy-Connection: close
    Content-Length: 2
    Connection: close
    服务端被访问 1 次

  差别只在一行,后果差很远:没有 `Proxy-Authenticate`,
  客户端只知道「被拒了」,不知道用哪种方案、哪个 realm,
  只能干瞪眼——它没法自己拼出 `Proxy-Authorization`。
  有了这一行,客户端才知道该发 `Basic realm="gw"` 对应的凭据。

  注意它和 401 的 `WWW-Authenticate` 是两套:
  407 说的是「代理要你认证」,401 说的是「目标服务器要你认证」。
  凭据也分两套,`Proxy-Authorization` 只发到第一跳代理,
  不会跟着请求一路传到源服务器——这一点 D082 已经实测过。

不带的那组,407 里只有 Proxy-Connection 和 Content-Length: 2,客户端拿不到方案名和 realm。 带的那组多一行 Proxy-Authenticate: Basic realm="gw",客户端这才知道该发 Basic 凭据去哪一区。 两组服务端都被访问 1 次——因为客户端没有能力发起第二次,差别不在请求次数上, 在「能不能迈出第二步」上。 这条字段只出现在 407 里。在 200 或 401 里见到它,基本可以判定是配错了。

30 秒实验

  1. 把 realm 改成中文,看响应字节数会多多少(提示:要按 UTF-8 算)。
  2. 把 Basic 换成 Digest,观察客户端会不会换一种拼法。
  3. 想一想:为什么代理认证的凭据绝不能转发到源服务器?

常见误区

  1. 「407 和 401 是一回事」——一个拦人的是代理,一个是目标服务器,凭据也分两套。
  2. 「Proxy-Authorization 要一路带到源站」——必须在这一跳就被剥掉。
  3. 「realm 是权限范围」——只是个显示用的名字,不参与校验。
自测三问(点开对答案) 1. 哪个状态码必须带 `Proxy-Authenticate`? —— 407 Proxy Authentication Required。 2. `Proxy-Authorization` 会被转发到源服务器吗? —— 不会,代理消费完必须删掉,否则凭据就泄露了。 3. realm 的作用是什么? —— 给保护区起个名字,登录框显示用,不参与鉴权。

认证拦人是硬拦。还有一种更客气的拒绝:「现在忙,等会儿再来」——那是 Retry-After。

相关笔记