资讯动态

本地 HTTPS 证书完全指南:OpenSSL 手写与 mkcert 实战

发布时间:2026/10/17 11:03:00 来源:尧图企业网站定制
做本地开发的人多少都被浏览器的“证书无效”页面拦过。最初我面对“本地 HTTPS 证书”这件事心里就一句话又不是上生产为什么要折腾直到后面连续踩了 PWA 必须跑在安全上下文、第三方登录回调不让用 http、Secure Cookie 在 http 下直接失效、以及同事通过局域网访问我电脑上的服务时浏览器一脸“这地址有问题”这些坑才明白本地环境同样需要一张能被信任的 HTTPS 证书。这篇文章就来聊聊我自建本地 HTTPS 证书的完整过程先用 OpenSSL 手工签一遍搞懂原理再用 mkcert 这种工具把日常开发效率拉满最后配合一张常见问题排查表帮你避开我趟过的那些坑。1. 为什么本地开发也躲不开 https 证书1.1 不是只有生产环境才需要 https很多人把 HTTPS 当成“上线之前要换的东西”但开发阶段里很多需求根本绕不开它。最典型的是安全上下文。浏览器里的 Service Worker、navigator.geolocation、getUserMedia摄像头/麦克风权限、navigator.credentials 这类 API 都要求页面处于安全上下文。虽然大多数浏览器把http://localhost视为安全上下文但一旦你换到http://192.168.x.x这种局域网地址或者用自定义域名访问本地服务这个“豁免”就不一定生效了。你需要 HTTPS 才能继续调试。另一个高频场景是 OAuth。各种第三方登录平台在配置回调地址时往往不接受http://明文回跳。就算本地调试模式允许http://localhost也经常遇到混合内容的问题页面本身是 HTTPS 的但请求里某个资源走的是 HTTP直接给浏览器拦下来。还有 Secure Cookie带Secure标志的 Cookie 只能通过 HTTPS 连接发送否则你就得准备一个无法登录的本地环境。所以本地生成 HTTPS 证书不是为了“模拟生产环境装样子”而是为了让登录态、浏览器安全特性、权限 API 和内网互访都能正常工作。这也是我把这篇文章里的流程当成每个前端/后端开发基本功的原因。1.2 自签名证书与正规 CA 证书到底差在哪浏览器判断一个 HTTPS 证书是否可信看的是“证书链能不能追溯到某个受信任的根证书”。正规证书由公开的 CA 签发这些 CA 的根证书预装在系统里浏览器一验就通。自签名证书则是你自己生成、自己签名没有厂商背书浏览器默认不认。大多数新手直接跑:openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365生成一张证书放到 Nginx 里打开浏览器一看照样报“您的连接不是私密连接”。原因有两个一方面这张证书没有导进系统信任库浏览器压根不认识签名者。另一方面单独一张自签名证书经常缺少 SANSubject Alternative Name主题备用名称而现代浏览器校验主机名已经不再依赖证书里的 CN 字段必须看 SAN。这就是为什么你明明把证书装好了访问https://localhost依然提示“此服务器无法证明它是 localhost”。正确的做法是走一条完整的“本地 CA 体系”先生成一个本地根证书把它导入系统“受信任的根证书颁发机构”再用这个根证书去签发站点证书并且在站点证书里写好 SAN。这样浏览器拿到站点证书后顺着链条找到本机里已信任的根证书校验通过就不会再闹情绪。这也正是接下来手写流程和 mkcert 工具所做的事情。1.3 先定方案手写 openssl 还是 mkcert两种办法我都在用方案优点缺点适合OpenSSL 手写完全可控、不依赖第三方、能理解证书底层逻辑命令繁琐容易漏 SAN信任导入要手动做学习原理、自动化签发、离线环境mkcert一条命令生成并自动信任、默认带 SAN、跨平台需要下载安装日常工具依赖日常开发、团队协作、快速出证书如果你只是想让本地服务尽快跑起来直接用 mkcert 没什么心理负担。但如果你是第一次接触这堆证书文件我强烈建议先跟着下一章用 OpenSSL 手撸一遍把“根证书、证书签名请求、SAN、信任锚”这些概念真正落到命令里。之后再切回 mkcert你会很清楚它在你机器上偷偷做了什么出了问题也知道从哪查。2. 用 OpenSSL 手工制作一张受本地信任的证书2.1 证书签发链路CA、私钥、SAN 的关系手工签发本地证书本质上是在履行一个“迷你证书机构”的工作。整条链路是这样的根证书Root CA私钥只有你自己有→ 用根证书私钥给站点证书签名 → 站点证书里写好要保护的域名/IP → 系统信任根证书 → 浏览器验证通过。这里最关键的是“信任根证书”这一步。站点证书本身不重要重要的是它由谁签名而浏览器只信任系统里预置的根证书。所以你手工做的第一件事不是生成站点的证书而是先造一个根证书把这个根证书导到系统信任区里。还有一点必须明确证书文件里的crt、pem、cer其实没有本质区别都是 X.509 格式的证书内容只是后缀名不同。.key是私钥.csr是证书签发请求。私钥文件一定要当成密码一样保管不能随便放公开目录。2.2 第一步生成本地根证书CA先建一个干净的目录推荐放在~/certs/dev-ca方便管理mkdir -p ~/certs/dev-ca cd ~/certs/dev-ca openssl genrsa -out dev-ca.key 2048 openssl req -x509 -new -nodes -key dev-ca.key -sha256 -days 3650 -out dev-ca.crt \ -subj /CNDev Root CA/OLocal Dev \ -addext basicConstraintscritical,CA:TRUE \ -addext keyUsagecritical,keyCertSign,cRLSign解释一下这些参数genrsa -out dev-ca.key 2048是生成 2048 位的 RSA 私钥。本地开发不需要上到 40962048 足够性能和安全性都合适。-nodes表示私钥不加密生成之后不需要每次输入密码开发环境图省事可以这么干如果是团队里规范一点建议去掉-nodes配合启动时输入密码。-days 3650给根证书十年有效期根证书一般不用频繁更新导入一次可以撑很久。-addext是给根证书加扩展属性。basicConstraintscritical,CA:TRUE表示这是一张 CA 证书keyUsagecritical,keyCertSign,cRLSign表示它的私钥可以给其他证书签名。这两个属性缺了某些系统即使在信任库里也不会把它当成合法根证书。如果你用的 OpenSSL 版本比较老不支持-addext可以写一个 ext 配置文件效果一样。2.3 第二步签发站点证书并与主机名匹配根证书生成后开始签站点证书。站点证书的私钥和根证书私钥必须分开不能共用。先生成站点私钥和签名请求openssl genrsa -out local-dev.key 2048 openssl req -new -key local-dev.key -out local-dev.csr \ -subj /CNlocalhost/OLocal DevCNlocalhost只是写一个方便识别的名称真正决定浏览器认不认的是下一份配置文件里的 SAN。创建local-dev.ext内容类似subjectAltNameDNS:localhost,IP:127.0.0.1,IP:::1,DNS:dev.local,DNS:*.l.test basicConstraintsCA:FALSE keyUsagedigitalSignature,keyEncipherment extendedKeyUsageserverAuth如果之后的笔记本电脑 IP 是192.168.3.50想用手机或者同事电脑直接访问也要把IP:192.168.3.50写进去。SAN 字段是浏览器匹配地址的唯一标准漏掉任何一个实际要访问的域名/IP都会导致访问时报错。然后用根证书和上一步的签名请求生成最终的证书文件openssl x509 -req -in local-dev.csr -CA dev-ca.crt -CAkey dev-ca.key \ -CAcreateserial -out local-dev.crt -days 398 -sha256 -extfile local-dev.ext-CAcreateserial会生成一个序列号文件用于区分历次签发的证书。有效期我写398天这是为了避免受当前浏览器对 TLS 证书有效期不得超过 398 天的限制。如果你写3650Chrome 有可能不接受白忙一场。到了这一步目录里应该有dev-ca.crt、dev-ca.key、local-dev.crt、local-dev.key。建议把私钥权限收紧chmod 600 local-dev.key dev-ca.key注意dev-ca.key是你本地信任体系的根它一旦泄露别人就可以用你的根证书签发任意网站证书让所有信任你本地根证书的设备都能被冒充。本地开发也要养成这个安全习惯。2.4 第三步把证书挂到本地服务上证书文件本身不生效还得让本地服务把证书用起来。以 Nginx 为例在 server 配置里加server { listen 443 ssl; server_name localhost; ssl_certificate /home/you/certs/dev-ca/local-dev.crt; ssl_certificate_key /home/you/certs/dev-ca/local-dev.key; # 其他配置... }配置文件改完后执行nginx -t检查语法然后systemctl reload nginx或直接重启。注意ssl_certificate填的是local-dev.crt不是dev-ca.crt。很多人图省事把根证书填进去结果发出去的是根证书浏览器没法把这个站点和域名对应起来。如果你用的是 Node.js 本地服务核心也是一样的const https require(https); const fs require(fs); https.createServer({ key: fs.readFileSync(./local-dev.key), cert: fs.readFileSync(./local-dev.crt) }, (req, res) { res.end(hello https); }).listen(443);其他工具比如前端开发服务器、Python 的python -m http.server、Go 服务、Java 服务配置思路都是指定证书路径和私钥路径。通行的做法是证书路径和公钥文件。同一个路径引用错了证书链就会断。2.5 第四步把 CA 导入系统信任库站点证书配置好后浏览器还是不放心因为你的根证书还没被系统信任。这一步是手工流程里最容易卡壳的地方。macOS 上我一般不开图形界面用命令行更稳sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/certs/dev-ca/dev-ca.crtWindows 上管理员权限运行 PowerShellcertutil -addstore Root dev-ca.crtLinux 的 Debian/Ubuntu 系可以拷贝到系统证书目录后更新sudo cp dev-ca.crt /usr/local/share/ca-certificates/dev-ca.crt sudo update-ca-certificates需要特别注意的是Firefox 默认不完全跟随系统信任库它自己有独立的证书库。如果测试环境是 Firefox在“设置 → 隐私与安全 → 证书 → 查看证书 → 证书颁发机构 → 导入”里手动选择dev-ca.crt勾选“信任此 CA 来标识网站”。这个和 Chrome 的处理不一样别漏了。移动端调试还有额外的一层Android 需要把 CA 证书放到“设置 → 安全 → 加密与凭据 → 安装证书”iOS 安装描述文件之后还要在“通用 → 关于本机 → 证书信任设置”里把根证书开关点亮。否则就算安装成功iOS 默认也不信任。3. 用 mkcert 把证书生成和信任导入一次搞定3.1 mkcert 替我们做了什么如果你的目标只是“赶紧把 HTTPS 跑起来”手工 OpenSSL 流程确实有点烦。mkcert 这个命令行工具本质上就是把上一章做的事情打包成一条命令它会在本地生成一个根 CA然后自动把根证书注册到系统信任库再用这个根证书签发你指定的域名/IP。你不用再记 openssl 扩展参数不用手动导入根证书也不容易漏掉 SAN。我在团队里给新人演示本地 HTTPS 方案时默认推荐 mkcert。它用起来几乎零门槛而且跨平台一致。最常被问的问题“mkcert 生成的证书能上生产吗”答案很明确不能。它生成本地根证书的信任范围只是这台机器/设备别人浏览器不带你这个根证书当然不会信任。生产环境请继续用正规 CA或者走 ACME 自动签发。3.2 安装、初始化和生成第一张证书macOS 上安装最简单brew install mkcertWindows 上可以用包管理器装也可以拿到预编译的二进制直接跑Linux 各发行版包管理器里多半也有。安装后先执行一次初始化mkcert -install这一步会生成本地根 CA并把根证书导入当前系统的信任区。执行完之后可以用mkcert -CAROOT查看生成的根证书保存在哪个目录。默认目录下会出现rootCA.pem和rootCA-key.pem前者是公钥证书后者是私钥。私钥同样要保护好不要提交到仓库。接着生成一个包含localhost和127.0.0.1的证书mkcert localhost 127.0.0.1 ::1命令跑完当前目录会多出两个文件默认文件名可能会根据参数生成比如localhost2.pem和localhost2-key.pem。我已经习惯手动指定文件名方便后续配置mkcert -key-file local-dev-key.pem -cert-file local-dev-cert.pem localhost 127.0.0.1 ::1然后把local-dev-cert.pem和local-dev-key.pem填到本地服务的 ssl 配置里就行。注意 mkcert 生成的证书里已经自动带上了 SAN所以https://localhost和https://127.0.0.1都能直接访问。3.3 多域名、多 IP 与自定义输出路径本地开发不止一个域名。像我在项目里经常要测自定义域名dev.local还要支持子域名api.dev.local、www.dev.local那就把它们都加到同一个证书里mkcert dev.local *.dev.local localhost 127.0.0.1 ::1*.dev.local是一张通配符证书能覆盖该域名下的任意子域名很适合本地方案。如果需要局域网 IP 访问也直接写mkcert 192.168.3.50 localhost 127.0.0.1 ::1生成后用mkcert -CAROOT可以找到刚才自动导入的根证书如果团队需要统一信任可以把rootCA.pem发给同事让他们手动安装或者在他们机器上直接mkcert -install后自动建立信任。这里有个小坑mkcert -install后生成的根证书在不同机器上是不同的如果希望团队里所有人的浏览器都信任同一套根证书需要把其中一台机器生成的rootCA.pem和rootCA-key.pem分发到mkcert -CAROOT目录。但这个操作有安全风险我更推荐“每台机器各装一次 mkcert各自维护本机信任”。3.4 配合前端开发服务器使用现在很多前端构建工具支持直接读证书文件。以 Vite 为例在vite.config.js里指定 HTTPS 选项import { defineConfig } from vite import fs from fs export default defineConfig({ server: { https: { key: fs.readFileSync(./certs/local-dev-key.pem), cert: fs.readFileSync(./certs/local-dev-cert.pem) }, host: 0.0.0.0 } })启动之后浏览器访问https://localhost:5173就不会再报错。如果项目里用的是 webpack-dev-server配置项类似同样提供https: { key, cert }。更简单一点也可以装对应的 mkcert 插件让构建工具启动时自动生成证书并注册信任省去手动维护文件。但用插件有时会把证书目录搞得很隐蔽我更喜欢在项目根目录放一个certs目录把生成的两个 pem 文件集中管理提交到.gitignore里其他同事拉下来后自己运行 mkcert 生成。4. 证书生成后最常见的翻车点与排查流程4.1 证书链验证通过浏览器仍然不信任遇到证书明明没问题浏览器还是报错首先要确认自己导入的是不是正确的根证书。常见错误是导入站点证书而不是根证书或者导入根证书时没有勾选“信任”。在命令行验证证书链是否合法openssl verify -CAfile dev-ca.crt local-dev.crt如果输出 OK说明证书链没问题。这时再去浏览器看证书详情点击“查看证书”确认“证书颁发机构”是不是你自己的根证书。如果浏览器里的信任列表找不到说明系统信任那一步没有真正生效。Windows 上常见的问题在于证书导入到了“当前用户”的受信任证书区域而浏览器进程以管理员身份运行看到的信任区域不一致。建议全部导入“本地计算机 → 受信任的根证书颁发机构”然后重启浏览器。另外Firefox 自带的证书库独立于系统如果只做了系统导入Firefox 依旧不买账需要单独把根证书导到 Firefox 的证书管理里。这是老大难别指望 Chrome 不报错Firefox 就一定不报错。4.2 访问 IP 或自定义域名提示“证书不安全”证书链没问题、根证书也被信任了但访问https://192.168.3.50依旧说“IP 地址不匹配”十有八九是 SAN 里没写这个 IP。现代浏览器已经不看证书的 CN 字段了只认 SAN 里的 DNS 或 IP。localhost 不会自动覆盖 192.168.3.50127.0.0.1 也不等于局域网 IP。排查手法很简单openssl x509 -in local-dev.crt -noout -text | grep -A1 Subject Alternative Name如果输出里没有你访问的那个地址就重新生成一份证书把对外的 IP/域名加进 SAN。mkcert 更直接缺了就重新执行mkcert localhost 127.0.0.1 192.168.3.50别图省事只写一个域名因为你的开发场景可能是“电脑上 localhost 能访问手机要用 IP 访问同一个服务”这时候证书必须同时包含两个地址。4.3 明明换了新证书浏览器还在用旧配置有时证书更新了浏览器访问还是旧信息甚至一直跳转到底部警告页。这多半是浏览器缓存或 HSTS 在捣乱。Chrome 里可以走chrome://net-internals/#hsts在 Delete domain 一栏输入本地域名后删除 HSTS 记录。HSTS 的作用是强制浏览器用 HTTPS 访问某个域名如果之前 HTTP 和 HTTPS 都试过浏览器可能已经记住了“以后必须 HTTPS”即使你关了服务器浏览器也会优先跳到 HTTPS然后因为证书不匹配报错。新证书生成后不要只是刷新页面建议先开一个无痕窗口访问或者完全退出浏览器再重新打开。Safari 的证书缓存尤其顽固重启后基本能好。如果 Nginx 那边配置了多份证书路径也可能因为配置写错而加载了旧证书用这条命令确认服务真正发送的是哪张证书openssl s_client -connect localhost:443 -showcerts /dev/null看输出中s:那一行的 CN/SAN就能直接判断服务端实际使用的是不是新证书。4.4 反向代理、Docker 端口映射里的证书坑把本地 HTTPS 证书放到反向代理和 Docker 场景里问题会更多一层。比如你有一个容器跑在 8080 端口Nginx 作为反代监听 443把请求转发给容器的 8080 端口。这时候证书里的域名必须对应外部访问的域名而不是容器内部的 service 名。否则浏览器访问https://localhost没问题但反代回源时如果校验上游证书可能报的是“证书和上游域名不匹配”。另一种情况是用 Docker 起 Nginx证书文件必须挂载进容器并且路径要正确。常见的错误是宿主机路径能看到但容器里的 Nginx 进程找不到导致启动失败或 502。我会专门把证书目录挂载进去例如volumes: - ./certs:/etc/nginx/certs:ro再检查 Nginx 配置里的路径是不是/etc/nginx/certs/...而不是宿主机路径。内网穿透或者映射到公网域名时自签名证书基本不适用。因为公网域名对应的访问者浏览器不信任你的本地根证书不可能要求用户手动导入。这种情况要么用真实域名证书要么用带证书透明的公网 HTTPS 隧道方案不要拿本地 CA 硬搞。5. 证书的日常维护与安全边界5.1 证书有效期怎么设到期怎么自动续本地证书也有有效期。根证书我习惯给十年站点证书给 398 天以内。不要嫌 398 天太短恰恰是短期证书才能逼你定期确认私钥没泄露、配置没过期。如果不想每次手工敲命令可以写一个简单的刷新脚本核心就是重新走一遍第 2 章的签发流程。mkcert 用户需要的更少重新执行一次mkcert命令覆盖文件即可。但要注意mkcert 不会自动清理旧证书最好在项目里约定好把旧 pem 文件删掉再生成。我通常会在package.json里加一个gen:cert脚本减少记忆成本{ scripts: { gen:cert: mkcert -key-file certs/localhost-key.pem -cert-file certs/localhost.pem localhost 127.0.0.1 ::1 } }证书到期前浏览器会给明显的告警自己不用记日期。但反向代理、Docker 内部使用的证书有时不会显示警告容易被忽略。建议日历加一条提醒有效期前两周重新签发。5.2 本地根证书和私钥绝不能带出去我在网上见过不少人把整包证书目录直接推到代码仓库里面既有rootCA.pem也有rootCA-key.pem这是非常危险的事情。根证书私钥一旦泄露任何人可以冒充你的本地 CA 签发证书所有信任这个根证书的设备都会相信伪造站点。保守做法是rootCA.pem公钥证书可以拿出来分享rootCA-key.pem私钥绝对留在自己机器站点证书的私钥local-dev.key同样只应该存在于你自己和需要部署的服务上。代码仓库里统一加.gitignore把*.key全部忽略*.key !sample.key另外不要把开发根证书导入到生产服务器也不要为了“方便”把某台内网机器的开发根证书复制到公网服务器。开发信任域和生产信任域分离是避免出安全事故的基本底线。如果生产环境真的需要内部证书请用独立的内部 CA不要拿开发环境的根证书凑合。5.3 团队里共享一套开发 CA 的正确姿势团队开发时如果每个人都各自执行mkcert -install那么每个人拥有的根证书都不同。这样并不是不行只是每个人都要手动信任自己本机上的根证书访问同事电脑的服务时容易出问题。更顺滑的做法是确定一套团队内统一的开发根证书由某一位成员生成根证书只把rootCA.pem和安装说明发给其他成员每个人导入系统信任库而站点证书各自在自己的机器上用同一根证书签发确保每个人的站点证书 SAN 覆盖自己机器的 IP。如果你不想共享私钥又希望同事之间能互访彼此电脑上的 HTTPS 服务比较合理的方式是每个人用自己的 mkcert 生成证书时统一把团队里常用 IP 列表加进去然后把各自的公钥证书放到一个内网同步目录。这样虽然根证书不同但每台机器互相导入对方证书作为站点信任证书也能工作只是维护成本高。我个人现在的习惯是根证书和私钥只放在电脑的~/certs/dev-ca目录Never 进仓库每个项目的证书统一生成到项目certs/目录并在.gitignore里忽略每次新同事加入先让他们mkcert -install再给他们一份证书生成命令模板。这样既保留了 SSL 功能又不会让信任体系变得越来越乱。本地 HTTPS 证书这件事解决一次后续收益很大。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑