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

Authorization 是客户端向源服务器证明身份用的。 最常见的 Basic 方案只是 base64,不是加密——它的唯一保护是「走 HTTPS」。

📝 本节和前面几节的关系

协商家族讲完了,从这一节开始讲身份。 注意它和 6.4.15 的 Proxy-Authorization 是一对,但目标不同。

一、格式:方案加凭据

值永远是两段:认证方案名,一个空格,然后是凭据。 Basic 是最简单的一种,凭据是「用户名:密码」的 base64。 Demo 里 bozai:secret 是 12 字节,base64 之后是 Ym96YWk6c2VjcmV0,16 字节。 膨胀 1.33 倍——因为 base64 每三个字节变成四个字符。 base64 不是加密。它没有密钥,任何人不带任何工具就能解回来。 Demo 里那一行「解回来是 'bozai:secret'」就是现场解的,用的还是标准库的 b64decode,一行代码。

二、为什么它还活着

因为简单。服务端不用管会话、不用签名、不用过期时间,每次请求带上就行——天然无状态。 代价是每个请求都要带密码。 这就是它必须配 HTTPS 的原因:明文 HTTP 上发 Basic,等于把密码写在明信片上。 另一个代价是没法主动登出。 客户端不发这个头就等于登出,但已经发出去的那些请求记录里,密码还是明文可解的。 所以真实系统里的做法是:用一次登录换一个短期令牌,之后带的是 Bearer 加令牌, 而不是 Basic 加密码。Bearer 不在 47 个字段的取值清单里,因为它是 OAuth 2.0 定义的方案名。

三、认证失败要回 401 加 WWW-Authenticate

不是 403。 401 是「你还没证明你是谁」,403 是「你是谁我知道了,但不许」。 这个区别在 4.4.2 和 4.4.3 已经说过,这里再落一次。 WWW-Authenticate 里必须写方案名,通常还带一个 realm(领域名)。 浏览器看到这一行才会弹那个原生的登录框。 realm 必须写 ASCII。Demo 里写的是 MySite。 写成中文会乱码——协议里的字符串默认按 latin-1 解,这个坑 4.4.2 踩过一次。

四、动手 Demo:四种情形:不带、正确、密码错、方案错

服务端要求 Basic 认证,realm 写 MySite。 客户端分别不带凭据、带正确凭据、带错误密码、带一个错误的认证方案(Bearer)去打它。

if not auth:
    return reply(u'HTTP/1.1 401 Unauthorized',
                 [(u'WWW-Authenticate', u'Basic realm="MySite"')],
                 b'need auth')
scheme, _, val = auth.partition(u' ')
if scheme.lower() != u'basic':
    return reply(u'HTTP/1.1 401 Unauthorized', ...)
plain = base64.b64decode(val.strip()).decode(u'utf-8')
u_, _, p_ = plain.partition(u':')
if u_ == USER and p_ == PASS:
    return reply(u'HTTP/1.1 200 OK', [], u'欢迎回来'.encode(u'utf-8'))

实跑输出:

--------------------------------------------------------------------
Authorization:Basic 只是 base64,不是加密
--------------------------------------------------------------------
  用户名密码拼起来是 `bozai:secret`,12 字节
  base64 之后是 `Ym96YWk6c2VjcmV0`,16 字节

    base64 是加密吗          不是。任何人不带密钥就能解回来
    解回来是                 'bozai:secret'

  【不带凭据】
    状态                     HTTP/1.1 401 Unauthorized
    正文                     need auth

  【正确凭据】
    状态                     HTTP/1.1 200 OK
    正文                     欢迎回来
    服务端解出               'bozai:secret'

  【错误密码】
    状态                     HTTP/1.1 401 Unauthorized
    正文                     wrong password

  【错误的认证方案】
    状态                     HTTP/1.1 401 Unauthorized
    正文                     bad scheme

  `WWW-Authenticate: Basic realm="MySite"` 里的 realm 必须写 ASCII。
  写成中文会乱码——因为协议里的字符串默认按 latin-1 解,
  这一点在 4.4.2 已经踩过一次。

  Basic 认证的唯一保护是「走 HTTPS」。明文 HTTP 上发 Basic,
  等于把密码写在明信片上寄出去——base64 连一层纸都算不上。

不带凭据:401,正文 need auth,配 WWW-Authenticate: Basic realm="MySite"。 正确凭据:200,正文「欢迎回来」,服务端解出来的就是 'bozai:secret'——原文,没有任何加密痕迹。 错误密码:401,正文 wrong password。 注意这里和服务端「查无此人」回了同一个状态码, 这是对的——不能让攻击者靠状态码区分「用户不存在」和「密码错」。 错误的方案(Bearer):401,正文 bad scheme。方案名不认就直接拒,连解都不解。

30 秒实验

  1. 把 realm 改成中文,重跑,看响应头的字节变成什么。
  2. 把 base64 那串改掉一个字符,看服务端解出来的是什么。
  3. 想一想:如果服务端同时支持 Basic 和 Bearer,WWW-Authenticate 该怎么写?

常见误区

  1. 「base64 是一种加密」——它是编码,可逆,且不需要密钥。
  2. 「Basic 认证失败要回 403」——是 401,配上 WWW-Authenticate。
  3. 「Basic 认证很安全因为密码被编码了」——唯一的安全来自 HTTPS。
自测三问(点开对答案) 1. `Basic` 方案的凭据是什么格式? —— 「用户名:密码」用 UTF-8 编码后做 base64。 2. 为什么说 base64 不是加密? —— 它没有密钥,decode 一行代码就能还原,只能防止肉眼直接读。 3. 认证失败该回 401 还是 403? —— 401 配 `WWW-Authenticate`,表示「你还没证明你是谁」。

身份说完了。下一个字段解决一个很实际的性能问题:正文值不值得发。

相关笔记