
做网站或者本地联调的时候绕不开一个让人又爱又恨的东西SSL 证书。尤其当你用 Nginx 这类 web 服务器托管站点一旦和 HTTPS 沾边就会连带出生成证书、配置加载、浏览器报警、证书不生效这一串连锁问题。我在本地开发环境、公司内网服务、公网站点上反复处理过这些流程今天想把“用 Nginx 生成证书、配置证书、让浏览器信任证书”这件事从头到尾拆开讲清楚帮那些卡在证书信任问题上的朋友少绕两圈。新手最容易陷入的误区是觉得 HTTPS 只要线上站点处理就够本地开发、测试环境完全不用管。但实际上本地环境才是踩坑重灾区。举个常见场景本地虚拟机里起了多个端口前端页面要调接口浏览器直接甩出“不安全连接”警告甚至把整页阻断调试效率直接被干没了。另一类场景是内网部署了工具服务明明填了证书Chrome 还是提示 net::err_cert_common_name_invalidd打开证书一看域名和 Common Name 对不上。这篇文章会以 Nginx 为主线兼顾本地多站点和公网证书续期从私钥生成一路带你走到浏览器绿锁。1. 证书到底在保护什么先分清 CA、公私钥和域名关系1.1 HTTPS 加密链路里证书更像一张身份证很多人的第一反应是把证书理解成一把加密锁。更准确的说法是证书是一种承载“绑定关系”的数字文件把“某个域名”和“某把公钥”绑在一起再由一个可信的机构CA签名证明这个绑定关系是真实有效的。真正负责加密动作的是公钥/私钥非对称算法证书本身不参与具体加密运算它只是告诉对方“我用的是这张身份证请验证我的身份”。浏览器访问一个 HTTPS 站点时会按顺序做三件事先拿到服务器送过来的证书看证书里的 Common Name 或 SAN 是否和当前访问地址一致然后沿着证书链向上找签发者一直追溯到操作系统或浏览器内置的根证书如果链路完整且可信浏览器才认为这次会话安全。这个机制也解释了为什么很多人把证书填进 Nginx 之后依然没有绿锁——不是证书文件没配置而是信任链没有闭合。1.2 CA、自签名证书和中间证书的关系CA 是证书颁发机构Certificate Authority它的根证书被各大操作系统和浏览器预先安装。平时在公网申请的免费证书都是由某一家 CA 的根证书签发出来的中间可能还隔着一层中间证书Intermediate Certificate。Nginx 配置时如果只填写站点证书文件不把中间证书补全浏览器就会提示证书链不完整。自签名证书则是自己给自己签发的证书没有上级 CA也不会被浏览器默认信任。开发者常用的 openssl req 直接生成的就是这种。它能加密通信但无法证明身份所以浏览器必然拦一道。三种典型证书形态的区别我整理成了一张表。证书类型谁来签发浏览器默认行为适用场景自签名证书自己签发不信任提示警告临时本地测试、快速验证自建 CA 签发自己的根 CA导入 CA 后全信任本地多站点、虚拟机联调、内网服务公网 CA 签发Lets Encrypt、云厂商 CA 等默认信任公网站点、对外开放的服务1.3 浏览器校验三件套域名、有效期、用途证书里有三个字段最容易被忽略有效期notBefore/notAfter、主题备用名SAN和扩展用途Key Usage / Extended Key Usage。浏览器会校验系统时间是否落在有效期内也会检查证书是否包含 serverAuth 用途。后端开发最容易踩的坑是系统时间偏差——服务器时间慢了几个月刚签的证书会被提前判定为“尚未生效”。这类问题不常见但一旦遇到很容易让人对着证书配置翻半天。2. 手动生成证书的完整操作从纯自签到本地 CA 双证书2.1 先准备好 openssl 和证书目录Linux 和 macOS 基本都自带 openssl输入 openssl version 就能确认。Windows 上通常两种方案安装 Git for Windows 后使用它自带的 openssl或者直接通过 WSL 执行命令。命令行虽然看着简陋但生成结果最可控不推荐额外下载第三方图形工具。我在生成证书前会先建一个独立目录例如 /etc/nginx/ssl 或 ~/certs把后续的私钥、证书、CSR 都放一起。这样 Nginx 配置时路径统一也方便后续脚本引用。2.2 一条命令生成自签名证书但别漏掉 SAN最简单的自签名证书两步就能完成openssl genrsa -out mydomain.key 2048 openssl req -new -x509 -key mydomain.key -out mydomain.crt -days 365 \ -subj /CCN/STBeijing/LBeijing/Odev/CNmydomain.local这里有一个非常关键的点-subj 中的 CN 必须填写实际访问域名。如果你访问的是 mydomain.localCN 就写 mydomain.local如果写成一个和域名无关的字符串浏览器会直接报证书名称不匹配。但上面的命令默认不包含 SANSubject Alternative Name而现代 Chrome 从 58 版本开始就不再信任仅靠 CN 匹配的证书所以正确做法是生成带 SAN 扩展的证书。我实践中最常用的是下面这条openssl req -new -x509 -key mydomain.key -out mydomain.crt -days 825 \ -subj /CCN/STBeijing/LBeijing/Odev/CNmydomain.local \ -addext subjectAltNameDNS:mydomain.local,DNS:localhost,IP:127.0.0.1这样证书里同时包含域名、localhost 和本机 IP本地联调时切换访问方式都能通过校验能省掉很多“换个地址又不可信”的折腾。2.3 公司内网需要提交 CSR私钥始终留在自己手里如果是在公司内部申请由内网 CA 签发的证书标准流程是本机生成私钥和 CSRCertificate Signing Request把 CSR 提交给 CA 管理员由 CA 签发证书然后拿回来部署。CSR 生成命令如下openssl req -new -key mydomain.key -out mydomain.csr \ -subj /CCN/STBeijing/LBeijing/Odev/CNmydomain.local \ -addext subjectAltNameDNS:mydomain.local,DNS:api.mydomain.local这个过程最大的好处是私钥从未离开本机传输的只是公开的申请信息不会泄露密钥材料。2.4 自建本地 CA用一个根证书管所有开发站点如果你的开发环境不只是一台机器而是像热词里提到的“本地加虚拟机多端口多站点”那逐个信任自签名证书会非常痛苦。更优雅的做法是先生成一个本地根 CA 证书再用这个根 CA 给每个站点签证书最后只需把根 CA 导入操作系统信任库所有由它签出的站点证书都会被浏览器一并信任。先生成根 CAopenssl genrsa -out rootCA.key 2048 openssl req -new -x509 -key rootCA.key -out rootCA.crt -days 3650 \ -subj /CCN/STBeijing/LBeijing/OLocalDevCA/CNLocalDev Root CA \ -addext basicConstraintscritical,CA:TRUE然后给任意站点签发证书时用这张根 CA 来签openssl x509 -req -in mydomain.csr -CA rootCA.crt -CAkey rootCA.key \ -CAcreateserial -out mydomain.crt -days 825 \ -extfile (printf subjectAltNameDNS:mydomain.local,DNS:localhost,IP:127.0.0.1)这个方案的长期收益很明显。以后新增一个站点只需要走一遍“私钥 CSR CA 签发”浏览器不用再点击“继续访问”整个开发环境的 HTTPS 调试体验接近公网站点。我第一次搭完自建 CA 后最大的感受是前期多花十分钟往后几个月都不用再看浏览器的红色警告页。2.5 生成后花十秒自查 SAN 与有效期证书生成后我习惯立即用 openssl 检查内容和有效期openssl x509 -in mydomain.crt -noout -text | grep -A1 Subject Alternative Name这条命令会输出 SAN 列表。我见过太多人把证书配到 Nginx 上浏览器报错最后回头查才发现 SAN 里漏写了域名。自查这一步花不了十秒但能省下大把排查时间。3. Nginx 接入证书多站点多域名的配置骨架3.1 先确认 Nginx 配置目录和验证命令Nginx 的安装方式直接影响配置文件路径。Linux 上用系统包管理器安装配置文件默认在 /etc/nginx/Windows 版的配置文件位于解压目录下的 conf/ 文件夹里。很多人改了配置不生效往往是 nginx.conf 和 vhost 目录没有正确关联。我的建议是无论什么平台先跑一遍 nginx -t 验证语法再用 nginx -s reload 重载配置。如果 nginx -t 提示找不到证书文件优先检查的是路径权限和文件是否存在而不是配置文件的结构。3.2 最小 HTTPS server 块listen ssl 是关键一个最基础的 HTTPS 站点配置长这样server { listen 443 ssl; server_name mydomain.local; ssl_certificate /etc/nginx/ssl/mydomain.crt; ssl_certificate_key /etc/nginx/ssl/mydomain.key; root /var/www/mydomain; index index.html; }ssl_certificate 指定的是证书文件路径如果证书文件包含完整证书链可以一次性写进同一个文件ssl_certificate_key 是私钥路径私钥文件权限最好设为 600只允许 root 或 Nginx 运行用户读取。另一个高频错误是 listen 端口漏了 ssl 参数写成 listen 443;这样 Nginx 会用明文 HTTP 解析 TLS 握手数据浏览器必然报错。3.3 本地多端口多站点hosts 文件与 server_name 的配合热词里提到的“本地加虚拟机多端口开发环境多站点自定义域名配置”核心思路很简单用不同的 server_name 区分站点用不同的 listen 端口区分服务再在本地 hosts 文件里把自定义域名指到 127.0.0.1 或虚拟机 IP。举个例子本机有前端页面和后端接口两个服务我想通过 https://app.dev.local 访问前端通过 https://api.dev.local:8443 访问接口。Nginx 里就可以写两个 server 块server { listen 443 ssl; server_name app.dev.local; ssl_certificate /etc/nginx/ssl/app.dev.local.crt; ssl_certificate_key /etc/nginx/ssl/app.dev.local.key; root /var/www/webapp; } server { listen 8443 ssl; server_name api.dev.local; ssl_certificate /etc/nginx/ssl/api.dev.local.crt; ssl_certificate_key /etc/nginx/ssl/api.dev.local.key; root /var/www/api; }这里有一个我踩过的坑如果浏览器访问 https://api.dev.local:8443证书 SAN 里必须包含 api.dev.local否则一定报证书名称不匹配。端口本身不参与证书匹配所以证书只需要管域名不需要管端口。hosts 文件里则对应加上两行127.0.0.1 app.dev.local 127.0.0.1 api.dev.local如果是虚拟机场景就把 IP 改成虚拟机的 IP 地址。这样多站点多端口的环境就能在浏览器里像公网一样用域名访问不再被 IP 和端口混合输入搞得晕头转向。3.4 HTTP 强制跳 HTTPS 的 301 写法现在很多站点希望用户无论输入 http 还是 https最终都落到 https 上。Nginx 里最常用的做法是保留一个 80 端口 server 块做跳转server { listen 80; server_name mydomain.local; return 301 https://$host$request_uri; }这样访问 http://mydomain.local 会被 301 跳到 https://mydomain.local。需要注意如果 https 页面里引用了 http 资源浏览器会提示 Mixed Content混合内容这类问题多半不是证书本身的问题而是外部资源的协议没有统一。3.5 顺手做好的 TLS 协议与会话参数Nginx 配置里还应该关注协议版本。老旧 TLS 1.0/1.1 已经不受主流浏览器支持我现在维护的站点一般只启用 TLS 1.2 和 TLS 1.3ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m;这些参数对浏览器是否报警没有直接影响但会影响安全评级和连接性能。ssl_session_cache 用来复用 HTTPS 会话减少重复握手开销对高并发场景价值很大。4. 让浏览器认账导入证书信任链的完整链路4.1 浏览器内置信任库根 CA 才是锚点浏览器对证书的校验策略是“宁可错杀不可放过”。它内置了一批根证书只有当站点证书的签发链路最终溯源到这些内置根证书时才会展示绿色锁标。自签名证书没有溯源到内置根所以浏览器弹警告自建 CA 签发的证书只要把那个根 CA 导入操作系统信任库整条链的末端就有了可信锚点。这里最关键的一步认知是导入的是根 CA 证书不是站点证书。很多开发者把站点证书直接导入发现浏览器还是不信任就是错误理解了信任链的方向。4.2 Windows 导入受信任根证书的两个步骤Windows 下导入证书最省事的方式是双击 .crt 文件弹出证书信息后点击“安装证书”存储位置选“本地计算机”然后选择“将所有的证书都放入下列存储”并点击“浏览”选择“受信任的根证书颁发机构”。导入完成后系统范围内的浏览器都会把该 CA 视为可信。自动化运维场景里也可以用命令行导入Import-Certificate -FilePath C:\certs\rootCA.crt -CertStoreLocation Cert:\LocalMachine\Root注意 Windows 证书导入向导里有一个弹窗问“是否要信任该证书”如果没有点“是”证书虽然装进去了但可能没有被标记为受信任浏览器依然会报警。4.3 macOS 需要额外打开“始终信任”macOS 操作稍微绕一些。双击证书会打开“钥匙串访问”把证书拖到“系统”钥匙串后还需要双击该证书展开“信任”选项卡把“使用此证书时”改成“始终信任”。这一步不做证书虽然入库但浏览器依然会提示不受信任。命令行方式如下sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain rootCA.crt很多人只拖入钥匙串却不修改信任设置导致“明明装了证书还是不绿”本质上是证书进入存储库了但信任开关没打开。4.4 Linux 桌面和服务器信任库的区别Linux 桌面环境碎片化不同发行版路径不同。Debian/Ubuntu 下常见做法是把 CA 放到 /usr/local/share/ca-certificates/ 目录然后执行sudo cp rootCA.crt /usr/local/share/ca-certificates/localdev-ca.crt sudo update-ca-certificatesCentOS/RHEL 则放到 /etc/pki/ca-trust/source/anchors/ 后执行 update-ca-trust。导入后 Chrome 通常会跟随系统信任库Firefox 则有自己的独立信任库需要在“隐私与安全 - 证书”里手动导入。这个差异也解释了为什么同一个系统里 Chrome 能正常访问Firefox 却依然报警。4.5 Android、iOS 移动端联调的偏差体验移动端联调要额外谨慎。Android 7 之后应用默认不信任用户安装的 CA 证书即使你在系统设置里导入了证书部分应用依然坚持校验失败如果只是用浏览器访问页面需要把 CA 证书作为“CA 证书”安装而不是“用户证书”。iOS 则是安装描述文件后还要在“设置 - 通用 - 关于本机 - 证书信任设置”中开启对根证书的完全信任。两步缺一不可否则移动端永远无法成功信任。5. 排查闭环证书不生效、浏览器报警的常见案例5.1 net::err_cert_common_name_invalid域名和证书对不上这个报错几乎占据了我见过的七八成案例。原因通常有三个方向一是证书里的 CN/SAN 没有包含当前访问域名二是访问时用了 IP但证书里只有域名没有 IP三是浏览器缓存了旧证书。前两种属于生成环节问题提前用 openssl x509 查询 SAN 就能避免第三种则需要在无痕窗口里核对或者多刷新几次。5.2 替换证书后仍显示旧证书链路配置与浏览器缓存两头堵这个案例在运营过一段时间的线上站点上很常见。明明把新证书写进配置也 reload 了访问时看到的还是旧证书。我的排查顺序是明确的先跑 nginx -t 确认配置没有语法错误。确认 reload 真实执行了。nginx -s reload 有时会因为 master 进程权限问题没有真正重载要观察执行后有没有报错输出。用 openssl s_client 直接连接端口查看 Nginx 实际给出的证书echo | openssl s_client -connect mydomain.local:443 2/dev/null | openssl x509 -noout -serial -subject -dates这条命令能拿到端口当前真实呈现的证书序列号和有效期比浏览器截图可靠得多。如果显示的还是旧证书那就检查 Nginx 是否 include 了多个同名 server 块后加载的配置可能把新配置覆盖了。5.3 ERR_SSL_PROTOCOL_ERROR 与 SSL_ERROR_RX_RECORD_TOO_LONG这类报错大多不是证书的信任问题而是 Nginx 根本没进入 TLS 握手流程。常见原因是 listen 端口只写了 443没有加 ssl或者浏览器访问的端口是 443但 Nginx 实际监听的是别的端口。修复很简单确认 server 块里写的是 listen 443 ssl;。另一个冷门原因是指向的证书文件是空文件或内容损坏Nginx 读到异常数据导致握手解析失败。5.4 证书链不完整合并中间证书的正确姿势公网证书或自建 CA 证书在浏览器里提示“证书链不完整”时处理方式是把中间证书合并进站点证书文件。合并顺序是站点证书在前中间证书在后cat mydomain.crt intermediate.crt fullchain.crt然后把 fullchain.crt 配置到 ssl_certificate。需要特别注意私钥不能参与拼接ssl_certificate_key 始终只指向站点私钥。还有人在 Windows 上用记事本编辑证书文件导致换行符变成 CRLFNginx 读取时可能解析异常。建议用命令行工具重新整理不要在纯文本编辑器里手工改。5.5 系统时间不对会让你白忙半小时这个坑很隐蔽。服务器系统时间如果和标准时间偏差超过几分钟浏览器就会根据证书有效期字段判定证书无效哪怕刚签发的证书也显示“不是有效证书”。排查时可以先用 date 看看系统时间再用 ntpdate 或 chrony 同步时间。我亲身经历过一次容器里的时间停在几个月前证书联调时折腾了半天最后才发现是时间源问题。5.6 用 openssl s_client 一锤定音最后的核对手段是完整输出一次握手结果openssl s_client -connect mydomain.local:443 -showcerts看输出里的 subject、issuer 和 verify return code。如果 verify 返回值不是 0说明校验链没有闭合verify return code: 21 表示无法验证第一个证书配合证书内容基本能定位问题究竟出在域名、有效期还是信任链。这一招在看远程服务器证书状态的时候尤其好用因为不需要登录到那台机器。6. 公网证书的免费路线与自动续期细节6.1 公网环境不能自签选 Lets Encrypt 还是云厂商免费证书公网环境不可能靠自建 CA 让所有访客信任必须使用浏览器内置信任的 CA。目前有两类主流入口一类是 Lets Encrypt 这类自动签发平台证书有效期 90 天另一类是云厂商提供的免费证书例如阿里云可以免费申请只是续期流程要在控制台操作。两者的共同点是都依赖 ACME 协议或类似机制完成证书签发与续期可以把它们理解为“证书自动兑换机”首次配置好之后后续只需在到期前换新。6.2 用 acme.sh 自动签发与续期配置一次到期自动换新我在公网服务器上更推荐 acme.sh 工具脚本化完成申请和续期几乎不用人工干预。基础流程是安装工具然后指定域名和站点目录申请证书curl https://get.acme.sh | sh bash ~/.acme.sh/acme.sh --issue -d mydomain.com -d www.mydomain.com --webroot /var/www/mydomain申请成功后把证书安装到 Nginx 并重载bash ~/.acme.sh/acme.sh --install-cert -d mydomain.com \ --key-file /etc/nginx/ssl/mydomain.key \ --fullchain-file /etc/nginx/ssl/fullchain.cer \ --reloadcmd systemctl reload nginxacme.sh 默认会写入 crontab到期前自动续期并调用 reloadcmd。这里有一个经验不要把证书直接放在 acme.sh 的默认输出目录让 Nginx 引用因为脚本升级或目录清理可能造成路径变化把证书复制到独立目录更省心。我压箱底的做法是把 Nginx 引用的证书路径固定为 /etc/nginx/ssl/每次续期后由脚本把新文件复制过去再执行 reload。6.3 云厂商证书的手动续期流程云厂商控制台一般会明确标注免费证书的有效期到期前需要手动进入续期入口操作。以阿里云的流程为例在证书控制台找到待续期的域名证书点击续期后需要重新验证域名所有权验证通过再签发新证书。新证书签发后把它下载成 Nginx 格式通常包含 .pem 证书文件和 .key 私钥文件再覆盖到服务器对应目录最后 nginx -t 并通过 nginx -s reload 加载。这个流程本身不复杂容易被忽略的是部分免费证书不支持通配符域名子域名一多就需要为每个子域名单独申请并配置。6.4 上线后的日常巡检与到期提醒证书配置完我习惯用两条路径验收第一条是本地浏览器直接访问观察地址栏锁标第二条是用在线检测平台查看证书链是否完整、协议版本和加密套件是否合格。日常巡检方面强烈建议把下面这条命令加入服务器的定时任务每天检查一次证书有效期openssl x509 -in /etc/nginx/ssl/fullchain.cer -noout -enddate脚本里解析输出中的 notAfter 字段如果到期天数少于阈值就发送告警。我自己的服务器就是靠这条命令加一个简单的判断脚本提前一周收到提醒再从容完成续期和 reload。自从搭好这套巡检机制我几乎没有再遇到线上服务因为证书过期而突然中断的情况。如果你也经常被证书问题折腾建议把从生成到巡检这套链路完整搭一次后面真的会轻松很多。