📘 第八章 确认访问用户身份的认证
📄 一句话概括

SSL 客户端认证就是双向 TLS:客户端也要出示证书,服务端验它的链和签名。 失败发生在握手阶段,连 HTTP 都还没开始。

📝 本节和前面几节的关系

前两节都在「知识」这一类里打转。这一节换成「持有」——凭据不是记住的,是放在设备里的。

一、四步握手

  1. 客户端先验证服务端证书(和普通 HTTPS 一样,这一步没变)。
  2. 服务端回一个 CertificateRequest,列出它信任的 CA。
  3. 客户端挑一张由该 CA 签发的证书发过去,并用对应私钥签一段握手数据。
  4. 服务端验这张证书的链、验那个签名,两样都过才继续握手。 注意第 2 步是「列出信任哪些 CA」,不是「列出信任哪些人」。 所以服务端不需要预先知道每个用户,只要他们都在同一个 CA 下面就行。

二、失败的层次不一样

Demo 里服务端设了 verify_mode = CERT_REQUIRED, 客户端不带证书时直接 ConnectionResetError——不是 401,是连接被重置。 带上证书的那次通过,收到 135 字节。两次的差别不在应用层,在握手层。 这一点很重要:客户端认证的失败不会走到 HTTP 状态码。 所以排查时看服务端日志会看到 TLS 握手失败,而不是 401; 如果有反向代理,日志可能落在代理而不是后端应用上。

三、适合谁用

适合机器对机器:私钥放在文件里,不需要人记,也不会被钓鱼。 适合高价值操作:银行后台运维、企业内部系统、物联网设备上报——设备天然适合放证书, 不适合让人天天输密码。 不适合面向大众的网站。原因不是技术不行,是分发和换发太麻烦——8.4.3 会用量出来的数字讲这件事。 还有一个细节:客户端证书里的身份是签发时定死的(比如 CN=bozai-client)。 一旦这张证书被复制走,服务端无法区分「本人」和「拿到文件的人」。 所以私钥的保护(比如放 TPM、放 U 盾)才是这套机制的安全边界。

四、动手 Demo:没证书连不上,有证书才谈 HTTP

用 openssl 造一个 CA、一张服务端证书、一张客户端证书; 服务端设 CERT_REQUIRED 并要求客户端出示证书;客户端分别带和不带证书各连一次。

sctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sctx.load_cert_chain(sv_p, sv_k)
sctx.verify_mode = ssl.CERT_REQUIRED        # 关键:要求客户端证书
sctx.load_verify_locations(ca_p)            # 信任哪个 CA

cctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
cctx.load_verify_locations(ca_p)
cctx.load_cert_chain(cl_p, cl_k)             # 客户端出示自己的证书

实跑输出:

--------------------------------------------------------------------
SSL 客户端认证:把证书挂到人身上
--------------------------------------------------------------------
  服务端设了 verify_mode=CERT_REQUIRED,也就是「没证书别谈」。
  客户端两种姿态各试一次:
    不带客户端证书                失败:ConnectionResetError
    带客户端证书                 通过,收到 135 字节

  差别发生在**握手阶段**,不在 HTTP 层。没证书的那次连 401 都收不到——
  TLS 握手本身就被服务端打断了,因为它在 ClientHello 之后要求客户端出证书。

  认证链条是这样的:
    1. 客户端先验证服务端证书(和普通 HTTPS 一样)
    2. 服务端回一个 CertificateRequest,列出它信任的 CA
    3. 客户端挑一张由该 CA 签发的证书发过去,并用私钥签一段握手数据
    4. 服务端验这张证书的链、验那个签名,两样都过才继续
  所以客户端认证的强度等于「私钥保管得多好」,跟密码无关。
  这也解释了为什么它适合机器对机器:证书放在文件里,不需要人记。

不带证书:ConnectionResetError——握手被服务端打断,连接直接断。 带证书:通过,收到 135 字节。 两次都没走到 401/403。客户端认证的成败在 TLS 层就定了,应用层甚至不知道自己被拒过。 这也是为什么它常和反向代理一起用:证书校验交给代理做,代理把「已认证的用户名」通过首部传给后端。

30 秒实验

  1. 把客户端证书的 CN 换成别的名字,看服务端能不能察觉(提示:它不检查,只看链)。
  2. 把 verify_mode 改成 CERT_OPTIONAL,再试不带证书的情况。
  3. 想一想:如果私钥文件被复制走了,服务端有什么办法区分吗?

常见误区

  1. 「客户端认证失败会回 401」——失败在握手层,看不到 HTTP 状态码。
  2. 「服务端要预先登记每个用户」——只要登记 CA,用户由 CA 签发。
  3. 「有证书就绝对安全」——证书被复制走就等于身份被复制。
自测三问(点开对答案) 1. SSL 客户端认证在第几步发生? —— 握手阶段,服务端发 `CertificateRequest` 之后。 2. 不带客户端证书会看到什么? —— 握手失败,连接被重置,不会有 HTTP 状态码。 3. 服务端校验客户端证书时看哪两样? —— 证书链能不能追到受信 CA,以及客户端用私钥签的那段数据对不对。

单靠证书还不够强。把它和密码叠起来,才是真正的双因素。

相关笔记