Node系列 · Node基础:https 模块
HTTPS = HTTP over TLS。在浏览器地址栏看到的锁图标、API 调用方与网关的加密通道、银行/支付接口的合规要求——全都靠 TLS。本章讲清楚 TLS 握手机制和 Node 中如何启用 HTTPS,包括本地开发必备的自签名证书。
一、HTTPS = HTTP + TLS
HTTPS = HTTP + TLS,本质是在 HTTP 协议下面再套一层加密传输层:
TLS(Transport Layer Security)解决三件事:
| 威胁 | TLS 解法 |
|---|---|
| 窃听:第三方能看到传输内容 | 对称加密(AES / ChaCha20) |
| 篡改:传输途中被修改 | 摘要 + 签名(MAC / AEAD) |
| 冒充:假装成服务端 | 服务端证书(CA 签名验证) |
二、TLS 握手流程
TLS 1.3 握手只需 1-RTT(1 次往返)。下面是简化版时序:
简化解读:
- 客户端:发支持的 TLS 版本、加密算法列表、随机数
- 服务端:选定算法、回随机数、附证书链(CA 签名过的服务端公钥)
- 客户端验证证书:用本地信任的 CA 公钥验证证书签名
- 密钥交换:客户端生成 pre-master secret,用服务端公钥加密传输;双方各自派生出会话密钥
- 加密通信:之后所有 HTTP 数据用会话密钥对称加密
TIP
TLS 1.2 是 2-RTT(两次往返),TLS 1.3 优化到 1-RTT 且强制前向保密。Node 默认启用 TLS 1.3。
三、证书是什么
X.509 证书本质是一段结构化数据:
Certificate:
Subject: CN=example.com
Issuer: CN=Let's Encrypt R3
Validity:
Not Before: ...
Not After: ...
Subject Public Key Info:
Public Key Algorithm: RSA / EC
Public Key: ...
Signature Algorithm: sha256WithRSAEncryption
Signature Value: ...证书里关键信息:
| 字段 | 含义 |
|---|---|
Subject | 证书持有者(域名 / 组织名) |
Issuer | 签发机构(CA) |
Validity | 有效期 |
Public Key | 服务端的公钥 |
Signature | CA 用自己私钥对证书内容的签名 |
WARNING
证书 = 公钥 + 身份信息 + CA 签名。客户端拿到证书后,用 CA 的公钥验证签名——如果通过,说明"这个公钥确实属于 example.com"。
四、自签名证书:本地开发必备
生产环境证书由 CA(Let's Encrypt / DigiCert 等)签发。本地开发需要自签名证书——自己签发给自己,浏览器会报警但能用。
4.1 用 openssl 生成
# 1. 生成私钥
openssl genrsa -out server.key 2048
# 2. 生成证书签名请求(CSR)
openssl req -new -key server.key -out server.csr \
-subj "/CN=localhost"
# 3. 用自己的私钥签发证书(365 天有效期)
openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# 4. 清理 CSR
rm server.csr4.2 一行命令生成(开发够用)
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout server.key -out server.crt -days 365 \
-subj "/CN=localhost"生成的两个文件:
| 文件 | 内容 |
|---|---|
server.key | 私钥(绝不能泄露) |
server.crt | 公钥证书(可对外分发) |
五、Node https 服务端
https 模块 API 与 http 几乎一致,区别只在 createServer 多传一个 options:
const https = require('node:https');
const fs = require('node:fs');
const options = {
key: fs.readFileSync('./server.key'),
cert: fs.readFileSync('./server.crt'),
};
const server = https.createServer(options, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('secure hello\n');
});
server.listen(3443, () => {
console.log('https://localhost:3443');
});$ curl -k https://localhost:3443
secure hello
# -k 表示跳过证书验证(因为是自签名)5.1 双向认证(mTLS)
客户端也要证书的场景(多用于服务间调用):
const options = {
key: fs.readFileSync('./server.key'),
cert: fs.readFileSync('./server.crt'),
ca: [fs.readFileSync('./client.crt')], // 信任的客户端 CA
requestCert: true, // 要求客户端证书
rejectUnauthorized: true, // 证书无效则拒绝
};六、Node https 客户端
6.1 用 https.request
const https = require('node:https');
const req = https.request(
{
host: 'api.github.com',
path: '/repos/nodejs/node',
method: 'GET',
headers: { 'User-Agent': 'node-https-client' },
},
(res) => {
console.log('状态:', res.statusCode);
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => console.log(body.slice(0, 200)));
}
);
req.on('error', (err) => console.error('请求失败:', err.message));
req.end();6.2 跳过证书验证(不要在生产用)
const req = https.request(
{
host: 'self-signed.example.com',
rejectUnauthorized: false, // ⚠️ 跳过证书验证
},
(res) => { /* ... */ }
);DANGER
rejectUnauthorized: false 只用于本地测试自签名证书。生产环境关闭它意味着失去"防冒充"能力——任何伪造的 CA 都能签出"信任"的证书。
6.3 自定义 CA
企业内网有自己的 CA:
const options = {
host: 'internal-api.company.com',
ca: fs.readFileSync('./company-ca.crt'),
};七、fetch 调用 HTTPS(Node 18+)
Node 18+ 内置的 fetch 也能直接走 HTTPS 协议——可以传自定义 https.Agent 来加 CA:
const res = await fetch('https://api.github.com/repos/nodejs/node');
const data = await res.json();
// 自定义 CA
const agent = new https.Agent({ ca: fs.readFileSync('./company-ca.crt') });
const res2 = await fetch('https://internal-api.company.com', { agent });八、TLS 性能与最佳实践
8.1 TLS 握手开销
每次新建 TLS 连接:
- 1-RTT 握手延迟(~100ms 跨国)
- 证书验证(CPU)
- 密钥派生(CPU)
缓解方案:
- 启用 TLS session resumption(session ticket)—— 握手过的客户端复用会话
- 用 HTTP/2 —— 单连接多路复用,减少握手次数
- 用 keep-alive —— 长连接复用
Node https.Agent 默认启用 session resumption。
8.2 证书有效期
Let's Encrypt 证书默认 90 天。建议自动化续期(certbot / acme.sh)。
过期后浏览器直接拦截,用户完全无法绕过(不像 HTTP 还能继续访问)。
8.3 SNI:一个 IP 多个证书
服务器可能服务多个域名:
const server = https.createServer({
SNICallback: (servername, cb) => {
// 根据 servername 返回不同证书
if (servername === 'a.example.com') {
cb(null, tls.createSecureContext({
key: fs.readFileSync('./a.key'),
cert: fs.readFileSync('./a.crt'),
}));
} else if (servername === 'b.example.com') {
cb(null, tls.createSecureContext({
key: fs.readFileSync('./b.key'),
cert: fs.readFileSync('./b.crt'),
}));
}
},
}, (req, res) => { /* ... */ });servername 来自 TLS 握手的 SNI 扩展,客户端用它告诉服务端"我要访问哪个域名"。
九、生产环境 HTTPS 架构
实际项目几乎不会让 Node 直接对外暴露 HTTPS 443:
理由:
- 证书和私钥放 Nginx 比放 Node 进程更安全(不易泄露)
- Nginx 处理 HTTPS 性能更高(epoll + 异步)
- Node 只关心业务,无需处理 TLS 加解密
但 Node 直接对外暴露 HTTPS 仍用于:
- 内部服务(VPN / 内网)
- 边缘函数 / Lambda
- 嵌入式设备
- 一次性 demo / 调试
十、常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
unable to verify the first certificate | 客户端找不到 CA | 传 ca 选项,或保证系统信任链完整 |
certificate has expired | 证书过期 | 续期 |
hostname/IP does not match certificate's altnames | 域名与证书不匹配 | 证书 CN/SAN 要包含当前域名 |
self signed certificate | 自签名证书未受信任 | 本地开发加 rejectUnauthorized: false;或把证书加入系统信任 |
TLS connection timeout | 网络或防火墙 | 检查服务器监听、放行端口 |
十一、小结
- HTTPS = HTTP over TLS,解决窃听 / 篡改 / 冒充三大威胁
- TLS 1.3 握手只需 1-RTT;流程是"密钥协商 + 证书验证 + 密钥派生"
- 证书 = 公钥 + 身份 + CA 签名;本地开发用 openssl 生成自签名证书
httpsAPI 与http一致;服务端多传{ key, cert },客户端可传ca- 生产环境 HTTPS 终止通常交给 Nginx / 云负载均衡,Node 只处理 HTTP 内网流量
- mTLS / SNI / session resumption 是高级场景,按需启用
