这个属性到底解决什么问题
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配合业务层校验更合适。
思考题
-
客户端设置
rejectUnauthorized:false后,如果authorized为false但你在回调里忘记destroy(),会发生什么?连接会保持吗?数据会发送到未验证的对端吗? -
服务端设置了
requestCert:true和rejectUnauthorized:true,但客户端没有配置证书。握手会在哪个阶段失败?触发的是服务端的error事件还是secureConnection回调中的authorized===false? -
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()等相邻方法的上下文,用于扩展证书检查能力。