You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Heroku部署Express无法调用req.socket.getPeerCertificate获取客户端证书

Heroku部署Node.js mTLS服务时SSL终止后获取客户端证书的方案

问题背景

  • 本地使用Node.js原生https模块搭配自签名服务端/客户端证书实现双向TLS认证(mTLS)时功能完全正常,部署到Heroku平台后相关逻辑失效
  • 根因是Heroku平台默认会在边缘路由节点统一完成SSL终止,向后端应用转发的是解密后的明文HTTP请求,应用无法直接通过https模块对外提供HTTPS服务获取TLS层的客户端证书信息
  • 故障现象:Heroku环境下req.socket是普通TCP套接字,不存在getPeerCertificate()方法,req.client.authorized字段也不存在,原有证书校验逻辑完全无法运行

本地可正常运行的HTTPS服务代码

const fs = require('fs');
const https = require('https');
const express = require('express');

const app = express();
const port = process.env.PORT || 4545;

app.get('/', (req, res) => {
    console.log(req.socket.getPeerCertificate());
    if (!req.client.authorized) {
        return res.status(401).send('You are not authorized');
    }
  return res.send('Hello, world!');
});

https
  .createServer(
    {
      requestCert: true,
      rejectUnauthorized: false,
      ca: fs.readFileSync('ca.crt'),
      cert: fs.readFileSync('server.crt'),
      key: fs.readFileSync('server.key')
    }, app)
  .listen(port, () => {
    console.log('listening on port ', port)
  });

Heroku部署的故障版本代码

const fs = require('fs');
const https = require('https');
const express = require('express');

const app = express();
const port = process.env.PORT || 4545;

app.get('/', (req, res) => {
    console.log(req.socket.getPeerCertificate());
    if (!req.client.authorized) {
        return res.status(401).send('You are not authorized');
    }
  return res.send('Hello, world!');
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});

客户端测试代码

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

process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0

const req = https.request(
  {
    hostname: 'localhost',
    port: 4545,
    method: 'GET',
    path: '/',
    cert: fs.readFileSync('client.crt'),
    key: fs.readFileSync('client.key')
  },
  res => {
    res.on('data', function(data) {
      console.log("recieved data ", data.toString());
    });
  }
);

req.end();

可行解决方案

方案1:使用Heroku原生客户端证书透传能力

该功能为Heroku付费dyno专属能力,免费层级不支持。开启后Heroku边缘节点会在TLS握手阶段接收客户端证书,将URL编码后的PEM格式证书内容通过X-Client-Cert请求头透传给后端应用,你需要在Express侧自行完成证书校验,替代原有TLS层的校验逻辑。
改造后的服务代码示例:

const fs = require('fs');
const express = require('express');
const crypto = require('crypto');

const app = express();
const port = process.env.PORT || 4545;
// 加载你信任的CA根证书
const trustedCA = fs.readFileSync('ca.crt');
const caCert = new crypto.X509Certificate(trustedCA);

app.get('/', (req, res) => {
  const clientCertHeader = req.headers['x-client-cert'];
  if (!clientCertHeader) {
    return res.status(401).send('No client certificate provided');
  }
  // 解码Heroku编码后的证书内容
  const clientCertPem = decodeURIComponent(clientCertHeader);
  try {
    const clientCert = new crypto.X509Certificate(clientCertPem);
    // 校验证书是否由信任CA签发
    const isTrusted = clientCert.verify(caCert.publicKey);
    // 校验证书是否在有效期内
    const isNotExpired = new Date(clientCert.validTo) > new Date();
    if (!isTrusted || !isNotExpired) {
      return res.status(401).send('Invalid client certificate');
    }
    // 校验通过,可从clientCert中提取主题、序列号等信息做身份识别
    console.log('Valid client certificate, subject:', clientCert.subject);
    return res.send('Hello, world!');
  } catch (err) {
    return res.status(401).send('Invalid client certificate format');
  }
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
});

注意:使用该方案时必须配置Heroku的路由规则,强制所有请求使用HTTPS,禁止HTTP明文访问。

方案2:端到端TLS穿透(无平台付费依赖)

如果不想使用Heroku的付费功能,可以配置TCP路由或者支持SNI穿透的路由规则,让TLS握手直接穿透Heroku边缘节点到达你的Node.js应用,此时你可以完全沿用本地运行的https.createServer逻辑实现mTLS,不需要修改原有证书校验代码。
如果没有配置TCP路由的权限,也可以在Heroku应用外层增加支持mTLS的反向代理层,由反向代理完成客户端证书校验,校验通过后再将请求转发到Heroku应用,反向代理通过自定义请求头传递证书校验结果和证书信息。

重要提醒:使用反向代理透传校验结果的方案,必须配置Heroku应用的访问规则,仅允许反向代理的IP段访问应用,避免攻击者伪造请求头绕过证书校验逻辑。

内容的提问来源于stack exchange,提问作者Zivxx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 16:18:21