devcert-node20
v1.2.4
Published
Generate trusted local SSL/TLS certificates for local SSL development (Node.js 20+ compatible)
Downloads
142
Readme
devcert - 让本地开发 HTTPS 更简单
本地跑 HTTPS 服务通常很麻烦。常见方案各有取舍,而最常见的自签名证书方案,往往意味着每个项目都要面对浏览器里吓人的不安全提示。
devcert 让这个流程更简单。需要给你的服务生成私钥和证书?直接这样用:
let ssl = await devcert.certificateFor('my-app.test');
https.createServer(ssl, app).listen(3000);然后打开 https://my-app.test:3000,你会发现页面可以直接正常访问,不再出现烦人的证书警告。
证书会按域名缓存,因此对
certificateFor('foo')的两次调用会返回同一份 key 和 cert。
项目说明
本项目基于原始 devcert 实现,并在此基础上进行了对 Node.js 20+ 的兼容性适配。
选项
devcert 在首次安装或升级时,会创建一个自签名根证书颁发机构(CA),并用它来签发后续的域名证书。它会尝试将该 CA 注册到 macOS、Linux、Windows 的系统信任存储中。
但需要注意,一些 HTTP 客户端(例如 Firefox、Node.js 本身)会使用自己的信任列表,而不是系统信任存储。为此,getCaPath 与 getCaBuffer 可以把 CA 信息附带在 certificateFor() 的返回结果中,供这些程序自行决定是否信任。
getCaPath
设置为 true 后,返回对象会包含 caPath 字段,值是 CA 文件路径。你可以把这个路径用于支持路径参数的本地信任配置,比如 Node.js 内置环境变量 NODE_EXTRA_CA_CERTS。
getCaBuffer
设置为 true 后,返回对象会包含 ca 字段,值是 CA 文件的 UTF-8 文本内容。适合那些不依赖操作系统信任存储、但支持直接传入证书内容的场景。
skipHostsFile
如果你申请的是自定义域名(即非 localhost),devcert 会尝试修改系统配置,把该域名解析到本机(而不是公网真实域名),默认通过修改 /etc/hosts 实现。
如果传入 skipHostsFile,则会跳过这一步。这意味着当你申请 my-app.test 这类证书时,如果没有其他 DNS 重定向手段,你将无法通过 https://my-app.test 访问本地服务。
请注意,SSL 证书是按域名签发的。如果你申请的是 my-app.test 的证书,那么访问 localhost 并不会生效,即使你的应用其实跑在本机上。
skipCertutil
这个选项会告诉 devcert 不要自动安装 certutil 工具。
certutil 是用于在某些场景自动安装 SSL 证书的工具,尤其是:
- Firefox(所有操作系统)
- Chrome(仅 Linux)
默认情况下,如果系统缺少 certutil,devcert 会尝试自动安装。若你不希望自动安装,可传入 skipCertutil: true。
传入后会影响以下场景:
- Firefox(全平台):Firefox 支持手动导入并信任证书。若设置了
skipCertutil: true,devcert 会改为自动打开 Firefox 并启动导入向导。按照提示完成即可。每台机器通常只需做一次。 - Linux 上的 Chrome:目前 Linux Chrome 基本只能通过
certutil信任证书,没有等价的手动流程。因此如果你在 Linux 上使用 Chrome,请不要设置skipCertutil: true,否则证书不会被 Chrome 信任。
certutil 的安装方式:
- Mac:
brew install nss - Linux:
apt install libnss3-tools - Windows: 不适用(Windows 上没有简单的无交互安装方式,因此 Firefox 会回退到向导流程)
多域名(SAN)
如果你在做多租户应用,或者本地有多个应用域名,可以通过传入域名数组来生成带 Subject Alternative Name 扩展的证书。
let ssl = await devcert.certificateFor([
'localhost',
'local.api.example.com',
'local.example.com',
'local.auth.example.com'
]);
https.createServer(ssl, app).listen(3000);Docker 与本地开发
如果你使用 Docker 开发,可以把 devcert 安装在用户目录下的独立工具目录中,为多个本地 Docker 项目统一生成证书。可参考 这个 issue 的讨论与注意事项。
虽然不是最优雅的方法,但通常只有在新增本地域名时才需要重新生成证书,频率一般不高。
示例脚本:
// 示例:在家目录创建如 ~/devcert-util 的目录
// ~/devcert-util/generate.js
const fs = require('fs');
const devcert = require('devcert');
// 如果只有一个域名,也可以用 devcert.certificateFor('local.example.com')
devcert.certificateFor([
'localhost',
'local.api.example.com',
'local.example.com',
'local.auth.example.com'
])
.then(({key, cert}) => {
fs.writeFileSync('./certs/tls.key', key);
fs.writeFileSync('./certs/tls.cert', cert);
})
.catch(console.error);一个简单用法是把生成后的 ~/devcert-util/certs 目录复制到各个 Docker 项目里:
# local-docker-project-root/
🗀 certs/
🗎 tls.key
🗎 tls.cert并在 .gitignore 里加入:
certs/这样这两个文件就可以被任意项目复用,不限于 Node.js。
在 Docker 中的 Node 服务里,直接读取证书并启动 HTTPS:
const fs = require('fs');
const Express = require('express');
const app = new Express();
https
.createServer({
key: fs.readFileSync('./certs/tls.key'),
cert: fs.readFileSync('./certs/tls.cert')
}, app)
.listen(3000);同样也适用于 webpack dev server 等工具:
// webpack.config.js
const fs = require('fs');
module.exports = {
//...
devServer: {
contentBase: join(__dirname, 'dist'),
host: '0.0.0.0',
public: 'local.api.example.com',
port: 3000,
publicPath: '/',
https: {
key: fs.readFileSync('./certs/tls.key'),
cert: fs.readFileSync('./certs/tls.cert')
}
}
};工作原理
当你请求开发证书时,devcert 会先检查这台机器是否已完成初始化。如果没有,它会创建根 CA,并将其加入系统和各浏览器的信任存储。你很可能会看到系统弹出的授权密码提示。
由于机器已经信任这个根 CA,因此后续由它签发的证书也会被信任。于是当你申请新域名证书时,devcert 会用根 CA 凭据签发该域名证书并返回给你。
如果某个域名之前已经签发过证书,devcert 会直接返回缓存结果。
因此浏览器不会再把这些证书标记为不受信任。
安全注意事项
当 devcert 安装根证书时,系统要求输入管理员密码是有原因的:一旦把根 CA 加入信任链,浏览器会信任它签发的任何证书。
这会带来潜在风险:如果他人能够访问 devcert 的 CA 凭据,并同时可以劫持或篡改你的网络流量,理论上就有可能伪装网站且不触发浏览器警告。
为降低此风险,devcert 会尽量确保他人无法在你不知情的情况下使用 CA 凭据。具体方式因平台而异:
- macOS 与 Linux:CA 凭据文件只允许 root 用户读取(例如
chown 0 ca-cert.crt、chmod 600 ca-cert.crt)。devcert 自身需要读写时会通过sudo完成。 - Windows:由于文件权限细节复杂,devcert 采用让用户输入密码并用 AES256 加密凭据的方式。该密码不会写入磁盘。
此外,凭据落盘时会使用临时文件,并在不再需要时立即删除。
根 CA 也是每台机器首次初始化时本地即时生成,因此不存在跨机器共享的中心密钥。
为什么要安装根 CA?
安装一个根 CA 能更容易管理多个本地域名的 SSL。另一种方案是为每个域名单独生成并信任自签名证书。
但问题在于:虽然 devcert 能帮助添加证书到信任存储,删除证书的工具链却并不总是完整覆盖所有场景。若你将来要彻底取消信任,可能需要手动逐个移除。
使用单一根 CA 后,禁用某个域名 SSL 会更简单:删除对应域名证书文件即可。因为这些域名证书本身不会被单独安装到信任存储,文件删掉就彻底失效。
集成
devcert 从设计上就是一个底层库,便于其他工具进行集成调用。目标是让不同框架、不同库都能更轻松地支持 HTTPS 本地开发。
如果你希望在自己的库、框架或 CLI 中集成 devcert,可以使用 ui 选项来自定义所有需要用户交互的节点(替换默认提示与界面),实现品牌化、国际化或融入已有工作流。
ui 选项是一个对象,可包含以下方法:
{
async getWindowsEncryptionPassword(): Promise<string> {
// Windows 下用于加密根 CA 凭据的密码。
// 如果用户输入错误,可能会被多次调用。
},
async warnChromeOnLinuxWithoutCertutil(): Promise<string> {
// Linux 下检测到 Chrome 且 skipCertutil=true 时触发。
// 用于提示用户 Chrome 将不会信任该证书。
},
async closeFirefoxBeforeContinuing() {
// 当需要写入 Firefox 信任存储且 Firefox 正在运行时触发。
// 由于 Firefox 退出时可能覆盖信任存储变更,需要先关闭 Firefox。
},
async startFirefoxWizard(certificateHost: string) {
// 检测到 Firefox 且指定 skipCertutil=true 时触发。
// devcert 即将启动 Firefox 的证书导入向导,可以在这里提前提示用户。
// certificateHost 是临时服务地址,用于触发证书下载流程并唤起向导。
},
async firefoxWizardPromptPage(certificateURL: string): Promise<string> {
// 启动 Firefox 导入向导时,Firefox 会先打开一个 HTML 页面。
// 该方法返回这个页面的模板内容,certificateURL 是实际证书地址。
// 默认实现是简单重定向;你也可以提供自定义页面。
}
async waitForFirefoxWizard() {
// 在启动导入向导后调用。
// 该方法应在用户确认向导完成后再 resolve。
}
}你可以只实现其中部分方法,未实现的方法会回退到默认实现。
测试
像 devcert 这种工具测试起来并不轻松。跨平台 GUI 自动化测试(尤其是浏览器证书相关流程与 Firefox 导入向导)很难完整覆盖。
为了方便测试,项目提供了一组虚拟机快照。每个镜像都处于“即将开始测试”的状态,启动后按 Enter 即可。
你也可以利用快照能力把虚拟机回滚到干净状态,开始下一轮测试。
注意:macOS 许可条款禁止在非 Apple 硬件上运行,因此测试 macOS 平台需要拥有 Mac。如果你没有 Mac,也没关系,在 PR 中注明无法测试 macOS 即可,维护者可以协助验证。
虚拟机快照
许可证
MIT © Dave Wasmer
