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 秒实验
- 把 realm 换成中文,看服务端要不要一起声明 charset。
- 并列加一行
WWW-Authenticate: Bearer realm="admin",看客户端选哪个。 - 想一想:为什么 403 里不该出现 WWW-Authenticate?
常见误区
- 「401 可以不写 WWW-Authenticate」——规范要求必须写,不写客户端没法重试。
- 「realm 是权限范围」——只是显示用的名字。
- 「403 也该带 WWW-Authenticate」——不该,403 是「身份已知但没权限」。
自测三问(点开对答案)
1. `Basic realm="admin", charset="UTF-8"` 三部分各管什么? —— 方案决定怎么拼凭据,realm 是显示名,charset 决定编码。 2. 401 少了 `WWW-Authenticate` 会怎样? —— 客户端拿不到方案,无法构造 Authorization,只能停在 401。 3. 为什么 403 不该带它? —— 403 表示身份已确认但没有权限,再认证一次也没用。响应首部九个讲完了。下一批是实体首部——那些真正描述「正文是什么」的字段。
- ☐ (本节完,下一节 6.6.1 Allow)