← crypto 模块 Node.js 调试器 →

Node.js TLS模块教程:tls.createServer、tls.connect与证书验证指南

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

1.TLS模块在Node.js中的定位

Node.js的tls模块是核心模块之一,底层依赖OpenSSL,不需要通过npm安装。它实现的是传输层安全协议,作用对象是TCP连接。理解这一点很关键:TLS保护的是传输通道,而不是应用层数据格式。HTTP、WebSocket、自定义二进制协议都可以跑在TLS之上。

SSL是上世纪九十年代的协议,已经废弃。现在口语里仍然有人说“SSL证书”,但实际使用的是TLS。Node.js的tls模块只实现TLS,不实现老旧的SSL版本。在2026年做新项目,不需要考虑兼容SSL。

一个常见误解是:只要用了HTTPS就等于安全。HTTPS只是HTTPoverTLS,如果证书验证被关闭,或者使用了自签名证书却不做校验,加密通道仍然可能被中间人接管。tls模块暴露了rejectUnauthorized、authorized、getPeerCertificate()这些接口,目的就是让开发者能明确控制验证行为。

2.TLS握手过程与Node.js的对应关系

TLS连接建立之前,客户端和服务器要完成握手。这个过程在Node.js里是自动进行的,但理解每一步有助于排查错误。

第一步,客户端发送ClientHello,包含支持的TLS版本、密码套件列表、随机数。Node.js客户端会根据tls.connect()的配置和底层OpenSSL能力生成这些内容。

第二步,服务器回复ServerHello,选定TLS版本和密码套件。如果客户端和服务器没有共同支持的版本,握手直接失败。

第三步,服务器发送数字证书。客户端用内置的根证书列表验证证书链。这一步决定了socket.authorized的值。

第四步,双方通过密钥交换算法生成会话密钥。现在TLS1.3使用ECDHE,提供前向保密。

第五步,握手完成,进入加密数据传输阶段。此后所有socket.write()和data事件都是密文。

在Node.js中,如果握手阶段出错,会在socket上触发error事件,错误信息通常包含UNABLE_TO_VERIFY_LEAF_SIGNATURE、CERT_HAS_EXPIRED、ERR_TLS_CERT_ALTNAME_INVALID等。这些错误名值得记住,排查时能快速定位问题。

3.生成测试用证书

在本地调试TLS服务之前,需要一对私钥和证书。OpenSSL命令如下:

openssl genrsa -out server-key.pem 2048
openssl req -new -x509 -key server-key.pem -out server-cert.pem -days 365

第一条生成2048位RSA私钥。第二条生成自签名证书,有效期一年。执行第二条命令时会提示输入国家、组织、CommonName等信息。CommonName在测试环境下可以填localhost,但如果客户端用IP连接,证书校验会失败,因为证书里的CN和实际访问的主机名不匹配。

代码号学习编程提示:自签名证书只适合开发和测试。生产环境应使用受信任CA签发的证书,或者内部CA统一管理。把自签名证书部署到生产环境并让客户端关闭验证,等于放弃了TLS的认证能力。

4.创建TLS服务器:tls.createServer

tls.createServer(options,listener)创建一个接受加密连接的TCP服务器。options中至少需要key和cert。

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

const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
};

const server = tls.createServer(options, (socket) => {
  console.log('安全客户端已连接');
  socket.write('欢迎访问 TLS 服务器');
});

server.listen(8000, () => {
  console.log('TLS 服务器运行在 8000 端口');
});

这段代码和net.createServer的结构很像,区别在于传入的socket是TLSSocket实例,数据在传输前已经被加密。socket上可以调用getProtocol()查看协商后的TLS版本,调用getPeerCertificate()获取对端证书。

项目经验:如果服务器同时需要处理普通TCP和TLS连接,不要写两个端口。可以使用tls.createServer的SNICallback或者在同一端口上通过server.on('secureConnection')区分。但更常见的做法是让反向代理(Nginx、Caddy)处理TLS终止,Node.js只监听本地HTTP。这样证书轮换、密码套件配置、HSTS都由代理层统一管理,Node.js进程不需要重启。

5.创建TLS客户端:tls.connect

tls.connect(options,callback)用于建立到TLS服务器的连接。callback在secureConnect事件触发时执行,表示握手完成。

const tls = require('node:tls');

const client = tls.connect({
  port: 8000,
  rejectUnauthorized: false
}, () => {
  console.log('已安全连接');
  console.log('TLS 版本:', client.getProtocol());
});

client.on('data', (data) => {
  console.log(data.toString());
  client.end();
});

rejectUnauthorized:false表示不验证服务器证书。这个选项在测试自签名证书时方便,但生产环境必须去掉或设为true。更稳妥的做法是把自签名CA加入ca选项:

const client = tls.connect({
  port: 8000,
  ca: fs.readFileSync('ca-cert.pem'),
  servername: 'localhost'
});

servername用于SNI,也参与证书主机名校验。如果连接的是IP地址,证书里必须有对应的IPSAN,否则会报ERR_TLS_CERT_ALTNAME_INVALID。

6.双向通信与Echo服务

TLS服务器和客户端可以互相发送数据。下面是一个安全的Echo服务。

服务器端:

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

const options = {
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
};

const server = tls.createServer(options, (socket) => {
  console.log('客户端已连接');
  socket.on('data', (data) => {
    socket.write(`Echo: ${data.toString()}`);
  });
  socket.on('error', (err) => {
    console.error('Socket 错误:', err.message);
  });
});

server.listen(8000, () => {
  console.log('安全 Echo 服务器运行在 8000 端口');
});

客户端:

const tls = require('node:tls');

const client = tls.connect({
  port: 8000,
  rejectUnauthorized: false
}, () => {
  client.write('Hello');
});

client.on('data', (data) => {
  console.log(data.toString());
  client.end();
});

输出:

Echo: Hello

这里有一个细节:TLSsocket的data事件和普通TCP一样,可能一次收到不完整的数据块。如果应用层协议有消息边界,需要自己处理粘包和拆包。TLS只保证传输加密和完整性,不保证消息边界。

7.常用方法与socket属性

方法或属性 说明
tls.createServer() 创建TLS服务器
tls.connect() 创建TLS客户端连接
tls.createSecureContext() 创建可复用的安全上下文
socket.getPeerCertificate() 返回对端证书详情
socket.getProtocol() 返回协商后的TLS版本
socket.authorized 表示对端证书是否验证通过
socket.authorizationError 验证失败时的错误原因

tls.createSecureContext()在需要为多个服务器或SNI场景复用密钥和证书时有用。它把key、cert、ca等配置封装成一个上下文对象,避免重复读取文件。

8.TLSS ocket事件

事件 触发时机
secureConnection 服务器端握手成功,建立安全连接
secureConnect 客户端握手成功
data 收到数据
end 对端发送FIN
error 发生错误
close 连接关闭
timeout 连接超时

服务器端监听secureConnection可以获取握手后的socket,做一些日志记录或访问控制。客户端监听secureConnect可以确认握手完成后再发送业务数据。

错误处理建议:error事件必须监听,否则进程可能因为未捕获的异常退出。在error回调里记录err.code和err.message,但不要记录密钥、sessionticket等敏感信息。

9.证书验证与生产环境注意事项

rejectUnauthorized默认是true。很多教程为了让示例跑起来,直接设为false,然后忘记改回来。这是生产环境常见的安全隐患。

正确做法有几种:

一是使用受信任CA签发的证书,客户端保持默认验证。二是内部服务使用私有CA,把CA证书通过ca选项传给客户端。三是使用checkServerIdentity自定义主机名校验逻辑,但除非有明确需求,否则不建议覆盖默认行为。

服务器端还可以要求客户端提供证书,实现双向TLS(mTLS):

const server = tls.createServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem'),
  requestCert: true,
  rejectUnauthorized: true,
  ca: fs.readFileSync('client-ca.pem')
}, (socket) => {
  if (socket.authorized) {
    console.log('客户端证书验证通过');
  } else {
    console.log('验证失败:', socket.authorizationError);
    socket.destroy();
  }
});

mTLS在微服务之间做身份认证时很实用,比共享密钥更安全。但它对证书管理要求更高,需要一套签发和轮换流程。

10.现在框架中的惯用写法

在Express、Fastify、NestJS中,通常不会直接在应用代码里调用tls.createServer。TLS终止交给Nginx、Caddy、云负载均衡器处理,Node.js只监听HTTP。这样做的原因包括:证书轮换不需要重启应用、代理层可以统一配置密码套件和HSTS、性能开销从Node.js进程转移到专门的代理。

如果确实需要在Node.js里启动HTTPS服务,可以使用https.createServer,它内部封装了tls。用法:

const https = require('node:https');
const fs = require('node:fs');

https.createServer({
  key: fs.readFileSync('server-key.pem'),
  cert: fs.readFileSync('server-cert.pem')
}, (req, res) => {
  res.end('secure');
}).listen(8443);

https模块的options和tls一致,只是请求处理变成了HTTP语义。WebSocket的wss也是类似,通过https服务器升级而来。

11.本节课程知识要点

  • tls模块是Node.js内置模块,基于OpenSSL,无需npm安装。

  • SSL已废弃,现在应用使用TLS。

  • tls.createServer创建TLS服务器,tls.connect创建客户端。

  • 证书验证默认开启,生产环境不要设置rejectUnauthorized:false。

  • 自签名证书适合测试,生产环境使用受信任CA或私有CA。

  • TLS保证传输加密和完整性,不保证消息边界。

  • 错误事件必须监听,避免进程异常退出。

  • 反向代理终止TLS是常见架构,Node.js不必直接处理证书。

12.练习与思考

  1. 用OpenSSL生成自签名证书,启动一个TLS服务器,用tls.connect连接并打印getProtocol()和getPeerCertificate()的结果。

  2. 把自签名CA加入客户端的ca选项,去掉rejectUnauthorized:false,验证连接是否成功。

  3. 实现一个mTLS示例,服务器要求客户端证书,客户端携带证书连接。

  4. 模拟证书过期或主机名不匹配,观察错误码,并写出对应的错误处理逻辑。

  5. 思考:在Kubernetes环境中,TLS终止应该放在Ingress还是应用进程内,各自有什么取舍。

Node.js的tls模块把OpenSSL的能力封装成事件驱动的API,用起来和普通TCP服务接近,但背后涉及证书、密钥交换、密码套件、主机名校验等一整套机制。实际项目中,理解握手流程和证书验证逻辑,比记住API参数更重要。把TLS放在架构的哪一层、证书怎么管理、验证失败怎么处理,这些决策直接决定了通信安全是否真实有效。

← Node.js crypto 模块教程:哈希、HMAC、AES加解密与密钥派生指南 Node.js调试器教程:--inspect、--inspect-brk、debugger语句与断点调试指南 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号