证书从哪来
给网站配 HTTPS,第一步是拿到证书。常见来源有三类:
- Let's Encrypt 免费证书:有效期 90 天,支持自动续期,个人项目和小团队用得最多;
- 云厂商的免费或付费证书:在控制台申请,下载后上传到服务器,有效期一年,到期要手动换;
- 企业 CA 签发的证书:需要提交资料审核,适合对外商业站点。
Let's Encrypt 的优势是自动化。用 certbot 申请,配合 cron 或 systemd timer 续期,基本可以做到"配一次,长期不用管"。缺点是有速率限制,反复申请同一个域名会被临时封禁。
申请方式推荐 webroot,不打断现有 Nginx:
sudo certbot certonly --webroot \
-w /var/www/certbot \
-d example.com -d www.example.com
前提是 Nginx 里已经把 /.well-known/acme-challenge/ 映射到 /var/www/certbot。证书会生成在 /etc/letsencrypt/live/example.com/ 下,其中 fullchain.pem 是证书加中间证书,privkey.pem 是私钥。
443 配置
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
}
}
几个选择说明:
ssl_certificate用fullchain.pem,不要用cert.pem。中间证书缺失时,浏览器可能正常,但 Android、Java 客户端会报证书链不完整;- 只开 TLSv1.2 和 TLSv1.3,老版本协议有已知漏洞;
ssl_session_cache减少重复握手,对高并发有帮助;- HSTS 告诉浏览器以后强制走 HTTPS,配置前确认所有子域名都支持 HTTPS,否则会把用户挡在门外。
证书路径要写 live/ 下的符号链接,不要直接指向 archive/ 里的实际文件。certbot 每次续期都会生成带序号的新文件,live/ 下的链接会自动指向最新版本,Nginx reload 后就能加载到新证书;写死 archive 路径,续期后链接的还是旧文件。
后端如果是 Spring Boot,记得让它知道原始协议:
server:
forward-headers-strategy: framework
否则应用生成的绝对链接、重定向地址可能还是 http://,前端会遇到混合内容警告。
自动续期
先手动跑一次演练,确认续期流程没问题:
sudo certbot renew --dry-run
正式续期用 cron:
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
--deploy-hook 只在证书真正更新时执行,避免每天无意义地 reload。也可以直接用 systemctl list-timers | grep certbot 查看系统自带的定时器,很多发行版安装 certbot 时已经配好了。
续期之后一定要 reload,否则 Nginx 还在用内存里的旧证书,浏览器看到的还是过期证书。
常见坑
1. 证书链不完整
只配了站点证书,没配中间证书。桌面浏览器可能自动补全,移动端和 Java 客户端直接握手失败。统一用 fullchain.pem。
2. 域名不匹配
证书只签了 example.com,用户访问 www.example.com 就报 NET::ERR_CERT_COMMON_NAME_INVALID。申请时把裸域和 www 都带上,或者用通配符证书。
3. 续期成功但没 reload
certbot certificates 显示证书有效期已经更新,openssl 看到的还是旧的。检查 --deploy-hook 是否配置、reload 命令是否有权限执行。
4. 80 端口被关闭导致验证失败
Let's Encrypt 的 HTTP-01 验证需要访问 80 端口。如果防火墙只开了 443,续期会失败。保留 80 端口做跳转和验证,或者改用 DNS-01 验证。
5. 服务器时间不准
证书有效期校验依赖系统时间。时间偏差太大,会出现"证书未生效"或"证书已过期"的误报。开启 NTP 同步,timedatectl 确认时间状态。
6. 混合内容
页面是 HTTPS,里面的图片、脚本、接口还是 HTTP,浏览器会拦截。检查后端返回的链接、前端写死的资源地址,统一改成相对路径或 HTTPS。
7. HSTS 配得太激进
max-age 设成一年且包含子域名,一旦某个子域名没有 HTTPS,用户会直接无法访问,而且很难回退。先在测试域名上验证,再逐步加长。
排查步骤
证书问题先用命令行确认,不要只看浏览器:
# 查看证书链、有效期、域名
openssl s_client -connect example.com:443 -servername example.com </dev/null \
| openssl x509 -noout -subject -issuer -dates
# 看完整的证书链
openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null
# 确认 HTTP 跳转
curl -I http://example.com
# 确认 HTTPS 响应
curl -vI https://example.com
如果 openssl 显示证书正常、浏览器报错,多半是证书链或本地时间问题;如果 curl 报 SSL certificate problem,看错误码里的 unable to get local issuer certificate(缺中间证书)还是 certificate has expired(过期)。再看 nginx -t 和 error.log,确认 Nginx 加载的是哪份证书文件。
评论