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

WWW-Authenticate 只出现在 401 里,说明「用什么方案、对哪个 realm 认证」。 它是 Basic 认证能跑起来的前提——没有它,客户端连密码该往哪儿放都不知道。

📝 本节和前面几节的关系

6.5.5 讲了代理那一侧的 407,这一节是目标服务器这一侧的 401。 两个字段长得像、位置像,用途完全不同。

一、三个组成部分

Basic realm="admin", charset="UTF-8" 拆开是三块:方案名 Basic、保护区名 realm="admin"、 字符集 charset="UTF-8"。 方案名决定客户端怎么拼凭据——Basic 是「账号:密码」再 base64;realm 决定登录框上显示哪一行字; charset 决定拼之前按什么编码转字节。 少了 charset,中文用户名在两边可能编出不一样的字节,认证就一直失败。 Demo 里 401 的响应首部就这两行:WWW-Authenticate 和 Content-Length: 6。 正文只有 6 字节 denied,真正有用的信息全在首部里。

二、没有它会怎样

Demo 跑了两组对照:不带这一行的,客户端停在 401 不动; 带的那组,客户端拼出 Authorization: Basic ... 重试,拿到 200。 差别不在服务端做了多少事,而在客户端「能不能迈出第二步」。 这也是自研认证最常见的坑:状态码对了、密码也没错,就是登不进去——因为 401 里缺了这一行。 顺带一句:401 必须带它,是规范里的硬要求(RFC 7235)。 反过来,403 不该带——403 的意思是「我知道你是谁,但你不能进」,再让你认证也没用。

三、可以并列多个方案

一个 401 里能出现多行 WWW-Authenticate,各报一个方案:Basic、Bearer、Digest。 客户端挑自己会的一个用。 排在前面的表示服务端更希望对方用哪个。这是「偏好」,不是「强制」——客户端完全可能挑后面那个。 今天的现实是 Bearer 一家独大:Authorization: Bearer <token>。 Basic 还活着,但基本只在内部工具和测试里出现,因为它等于明文传密码。

四、动手 Demo:带不带 WWW-Authenticate,差一次重试

同一个 401 处理器跑两遍,开关只有「响应里有没有 WWW-Authenticate」, 看客户端能不能自己拼出 Authorization 再试一次。凭据用 base64 手工拼,不用任何库。

cred = base64.b64encode(b'bozai:secret').decode('ascii')

def make(with_hdr, realm):
    def fn(raw, st):
        has = any(ln.lower().startswith(u'authorization:')
                  for ln in X.head_lines(raw))
        if not has:
            hd = []
            if with_hdr:
                hd.append((u'WWW-Authenticate',
                           u'Basic realm="%s", charset="UTF-8"' % realm))
            return X.reply(u'HTTP/1.1 401 Unauthorized', hd, b'denied')
        return X.reply(u'HTTP/1.1 200 OK', [], TEXT)
    return fn

实跑输出:

--------------------------------------------------------------------
WWW-Authenticate:401 必须说清「怎么认证」
--------------------------------------------------------------------
  带 WWW-Authenticate  客户端能重试   结果
  False                否             停在 401
  True                 是             OK

  带上的那一组,看看 401 里到底写了什么:
    WWW-Authenticate: Basic realm="admin", charset="UTF-8"
    Content-Length: 6
    Connection: close

  `Basic` 是方案名,`realm="admin"` 是保护区名字,
  `charset="UTF-8"` 告诉客户端账号密码按什么编码转字节。
  三项都缺一不可——少了方案名客户端不知道拼什么,
  少了 realm 弹窗里没法显示「你在登录哪里」。

  一个 401 里可以并列多个方案:
    WWW-Authenticate: Basic realm="admin"
    WWW-Authenticate: Bearer realm="admin"
  客户端挑一个自己会的。服务器排在前面的,是它更希望对方用的。

  顺带说清 realm 不是安全措施:它只是一个字符串,
  服务器爱写什么写什么,客户端也照显不误。真正的隔离靠的是凭据校验。

不带的那组:第一次 401,客户端没有方案信息,停在原地,结果是「停在 401」。 带的那组:第一次 401 拿到 Basic realm="admin", charset="UTF-8",拼出凭据再发一次, 状态码变成 200。 看 401 的完整首部,只有两行:WWW-Authenticate 和 Content-Length: 6,正文 6 字节。 信息全在首部,正文只是占位。 这也说明一件事:认证失败时,正文写多漂亮都没用,客户端要的是那行方案声明。

30 秒实验

  1. 把 realm 换成中文,看服务端要不要一起声明 charset。
  2. 并列加一行 WWW-Authenticate: Bearer realm="admin",看客户端选哪个。
  3. 想一想:为什么 403 里不该出现 WWW-Authenticate?

常见误区

  1. 「401 可以不写 WWW-Authenticate」——规范要求必须写,不写客户端没法重试。
  2. 「realm 是权限范围」——只是显示用的名字。
  3. 「403 也该带 WWW-Authenticate」——不该,403 是「身份已知但没权限」。
自测三问(点开对答案) 1. `Basic realm="admin", charset="UTF-8"` 三部分各管什么? —— 方案决定怎么拼凭据,realm 是显示名,charset 决定编码。 2. 401 少了 `WWW-Authenticate` 会怎样? —— 客户端拿不到方案,无法构造 Authorization,只能停在 401。 3. 为什么 403 不该带它? —— 403 表示身份已确认但没有权限,再认证一次也没用。

响应首部九个讲完了。下一批是实体首部——那些真正描述「正文是什么」的字段。

相关笔记