← stream.Writable.destroy() 方法 tlsSocket.isSessionReused() →

Node.js tlsSocket.authorized属性详解:TLS证书验证状态与authorizationError错误处理

著
原创 2026-10-20 Node.js 已有人查阅

这个属性到底解决什么问题

TLS握手完成不等于“对端可信”。握手只保证通道加密,证书验证才决定你正在通信的对象是否就是它声称的那个人。tlsSocket.authorized就是Node.js给出的验证结论:一个布尔值,指示对端证书是否由你指定的CA签发并通过验证。

它的配套属性tlsSocket.authorizationError在authorized===false时提供失败原因。两者必须一起使用,单独依赖任何一个都会导致错误处理不完整。

有一个容易被忽略的事实:authorized为false时,连接可能仍然处于加密状态且可用。TLS握手本身可以完成,证书验证失败并不会自动中止连接,除非你显式配置了rejectUnauthorized:true(客户端默认值)或服务端的rejectUnauthorized:true。这意味着:如果你只在回调里检查了authorized却没做任何处理,连接会继续运行在“加密但身份未确认”的状态下。

tlsSocket.authorized的读取时机与两端差异

客户端场景

tls.connect()的回调在握手完成后触发。此时读取socket.authorized即可判断服务端证书是否由ca选项指定的CA签发。

const tls = require('tls');
const fs = require('fs');

const socket = tls.connect({
  host: 'api.example.com',
  port: 443,
  ca: [fs.readFileSync('/etc/ssl/internal-ca.pem')],
  servername: 'api.example.com'
}, () => {
  if (!socket.authorized) {
    console.error('服务端证书验证失败:', socket.authorizationError);
    socket.destroy();
    return;
  }
  console.log('服务端证书可信,CN:', socket.getPeerCertificate().subject.CN);
});

客户端的rejectUnauthorized默认为true。在这种默认配置下,验证失败会直接触发error事件,authorized属性根本来不及被检查。要让authorized发挥“软检查”作用,必须显式设置rejectUnauthorized:false,然后在回调中自行判断。

服务端场景

服务端的authorized反映的是客户端证书的验证结果。前提是服务端启用了requestCert:true,否则客户端不会发送证书,authorized始终为false(因为没有对端证书可验证)。

const server = tls.createServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  ca: [fs.readFileSync('client-ca.pem')],
  requestCert: true,
  rejectUnauthorized: false  // 先接受连接,再手动裁决
}, (socket) => {
  if (!socket.authorized) {
    console.warn('客户端未通过验证:', socket.authorizationError);
    // 可以根据错误类型决定是否放行或断开
    socket.destroy();
    return;
  }
  socket.write('已通过双向认证\n');
});

把rejectUnauthorized设为false而保留requestCert:true,是一种“先握手、后决策”的策略。它允许你在连接建立后读取证书信息(如CN、fingerprint)进行业务层面的二次判断,而不是在握手阶段直接拒绝。

authorizationError的常见值与实际含义

当authorized为false时,authorizationError返回的字符串通常对应OpenSSL的验证错误码。以下是最常遇到的几类:

authorizationError 含义 典型场景
UNABLE_TO_VERIFY_LEAF_SIGNATURE 无法验证叶子签名 服务端未发送中间CA证书
UNABLE_TO_GET_ISSUER_CERT_LOCALLY 找不到本地信任的签发者 使用私有CA但未配置ca选项
SELF_SIGNED_CERT_IN_CHAIN 证书链中存在自签名证书 自建服务未正确配置信任链
DEPTH_ZERO_SELF_SIGNED_CERT 对端证书是自签名的 未配置CA的测试环境
CERT_HAS_EXPIRED 证书已过期 服务端证书续期遗漏
ERR_TLS_CERT_ALTNAME_INVALID 证书SAN与连接主机名不匹配 servername设置错误或证书域名不符

前四个错误与证书链信任有关,后两个分别涉及时效性和主机名校验。authorizationError的值不保证跨Node.js版本一致,但它来源于OpenSSL的验证结果,在同类错误上具有稳定性。

一个容易踩的坑:浏览器能访问不等于Node.js能验证

这是实际项目中最常引发困惑的现象。浏览器打开目标HTTPS站点正常,Node.js客户端却报UNABLE_TO_VERIFY_LEAF_SIGNATURE。

原因在于:浏览器有“中间证书自动补全”机制(AIAFetching),当服务端没有发送完整的证书链时,浏览器会自己去下载缺失的中间证书。Node.js不会做这件事,任何版本都不会。它严格依赖服务端在TLS握手中发送的证书链,以及本地配置的信任根。

遇到这个错误时,正确的修复位置在服务端:确保发送的是完整链文件(例如Let‘sEncrypt场景使用fullchain.pem而非cert.pem)。在客户端一侧临时绕过(设置NODE_TLS_REJECT_UNAUTHORIZED=0)只是把验证问题掩盖掉,不应作为生产环境的处理方式。

性能与对比:authorized检查本身没有开销,但配置策略有影响

tlsSocket.authorized的读取是纯属性访问,不触发额外计算或I/O,开销可忽略。真正影响性能的是围绕它的配置选择:

rejectUnauthorized:truevsfalse:前者在握手阶段由OpenSSL完成验证并直接拒绝无效证书,失败路径更短,不会建立可用的应用层连接。后者允许握手完成,由JavaScript层判断,失败时你已经持有一个加密socket,需要显式destroy(),否则可能泄漏资源。在恶意客户端较多的公网服务端场景,优先使用rejectUnauthorized:true把拒绝动作留在握手层。

服务端requestCert:true的代价:服务端会向客户端发送CertificateRequest,客户端需要读取并发送自己的证书。在mTLS场景下这是必要的;但如果你并不需要客户端证书,开启requestCert只会增加一次无意义的往返和证书处理开销。

常见误区速查

误区一:authorized===true就代表连接绝对安全。它只代表证书链验证通过。如果对端私钥泄露、证书被滥用,或者你信任的CA本身被攻破,authorized仍然是true。它是必要非充分条件。

误区二:服务端不设requestCert:true也能拿到客户端证书。不会。TLS协议中服务端必须显式请求,客户端才会发送证书。没有requestCert,authorized恒为false,getPeerCertificate()返回空对象。

误区三:rejectUnauthorized:false等于关闭加密。错误。它只关闭证书验证,TLS通道仍然加密。这是它能被用于“先连接后判断”策略的原因,也是它危险的原因:攻击者如果能够中间人拦截,加密通道的另一端可能不是你以为的服务。

误区四:authorizationError在authorized===true时也有值。官方文档明确说明该属性仅在authorized===false时设置。不要依赖它在成功时的状态,那可能是undefined或残留值。

用指纹做二次校验:比CA信任更细粒度的控制

在内部服务网格或CI/CD流水线中,CA信任范围可能过宽。这时可以在authorized为true的基础上,用getPeerCertificate().fingerprint与预期值比对,实现证书级别的白名单。

const ALLOWED_FINGERPRINTS = new Set([
  'AA:BB:CC:...',
  'DD:EE:FF:...'
]);

if (socket.authorized) {
  const fp = socket.getPeerCertificate().fingerprint;
  if (!ALLOWED_FINGERPRINTS.has(fp)) {
    socket.destroy();
    return;
  }
}

fingerprint是证书的SHA-1哈希,在证书有效期内稳定。但注意:证书续期后指纹会变,需要维护更新机制。对于需要长期稳定的场景,改用subject.CN或SAN配合业务层校验更合适。

思考题

  1. 客户端设置rejectUnauthorized:false后,如果authorized为false但你在回调里忘记destroy(),会发生什么?连接会保持吗?数据会发送到未验证的对端吗?

  2. 服务端设置了requestCert:true和rejectUnauthorized:true,但客户端没有配置证书。握手会在哪个阶段失败?触发的是服务端的error事件还是secureConnection回调中的authorized===false?

  3. authorizationError返回UNABLE_TO_VERIFY_LEAF_SIGNATURE时,客户端的ca选项应该包含什么?是根CA还是中间CA?

现在框架中的惯用写法

在Fastify或Express中直接使用tls.createServer的场景在减少。更常见的做法是让反向代理(Nginx、Envoy、ALB)终止TLS并处理客户端证书验证,应用层从转发头中读取验证结果。例如AWSALB的mTLS模式会在X-Amzn-Mtls-Clientcert-Status头中传递验证状态。此时应用代码不再依赖tlsSocket.authorized,而是检查代理注入的信任标记。了解这个属性的底层行为,仍然有助于你在代理配置出错时快速定位问题:代理的拒绝策略、CA链配置、证书转发格式,本质上与tlsSocket.authorized所依赖的验证逻辑一致。

延伸阅读与参考文献

Node.js官方文档·tlsSocket.authorized
https://nodejs.org/api/tls.html#tlssocketauthorized
最权威的属性定义与行为说明,涵盖客户端与服务端两种上下文。

Node.js官方文档·tls.connect()
https://nodejs.org/api/tls.html#tlsconnectoptions-callback
rejectUnauthorized与ca选项的官方语义,理解客户端验证默认行为的起点。

Node.jsUndici文档·Clientcertificate
https://undici.nodejs.org/best-practices/client-certificate
展示了authorized与authorizationError在mTLS场景下的实际用法,以及rejectUnauthorized:false的“先连接后判断”模式。

OpenSSL文档·openssl-verify
https://docs.openssl.org/master/man1/openssl-verify/
authorizationError中字符串所对应的OpenSSL验证错误码的原始定义。

Node.js官方文档·tls.TLSSocket
https://nodejs.org/api/tls.html#class-tlstlssocket
getPeerCertificate()、renegotiate()等相邻方法的上下文,用于扩展证书检查能力。

← stream.Writable.destroy() 方法 tlsSocket.isSessionReused() →
分享笔记 (共有 篇笔记)
验证码:
微信公众号