<!DOCTYPE html>
Nginx 强制 HTTPS 跳转实战:301/302 选型、HSTS 配置与常见排错
证书部署完成只是 HTTPS 改造的第一步,真正让全站"只走加密通道"靠的是强制跳转。本文从 301/302 的选型讲起,给出 Nginx 常用的四种跳转写法,覆盖泛域名、宝塔面板、Apache、Caddy 以及反代/CDN 场景,最后附带一套可直接照抄的排错命令清单,帮你避开 ERR_TOO_MANY_REDIRECTS 这类最常见的坑。
一、为什么需要强制跳转:明文端口与 301/302 的选型
默认情况下,Nginx 同时监听 80(HTTP 明文)与 443(HTTPS 加密)两个端口,用户手动输入域名或点击旧链接时仍会走 80 端口,密码、Cookie 全部裸奔,还可能被运营商插入广告。强制跳转的本质是:凡是命中 80 端口的请求,一律返回一个重定向响应,让浏览器自动改用 HTTPS 访问。
重定向响应主要分两种状态码,选错会直接影响 SEO 与线上业务:
- 301 Moved Permanently(永久重定向):搜索引擎会丢弃旧地址、合并权重到新地址,适合站点已经长期稳定走 HTTPS 的场景,是全站强制跳转的默认选择。
- 302 Found / 307 Temporary Redirect(临时重定向):浏览器与搜索引擎都保留旧地址,权重不迁移。适合 A/B 测试、临时维护页、灰度发布等"随时可能撤销"的场景。
另外还有一个容易忽略的细节:301 之后浏览器会缓存跳转结果,排错时看到"明明改了配置还是跳错地方",多半就是本地缓存了 301,需要用隐身窗口或 curl 验证。
二、Nginx 四种常用的强制跳转写法
写法一:独立 80 server 块 + return 301(生产环境首选)
把 80 端口单独拆成一个 server 块,职责单一、性能最好。return 直接在返回阶段结束请求,不走任何 location 匹配,是最高效的跳转方式。
server {
listen 80;
server_name www.example.com example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name www.example.com example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# 其余站点主体配置(root、location 等)
}
注意两点:$host 取的是 Host 头而非配置里的 server_name,泛域名、多域名都能自动匹配;$request_uri 保留原始 URI 和查询参数,确保用户从 http://example.com/path?k=v 跳过去后链接不丢参数。
写法二:rewrite 方式(不推荐,仅作对比)
server {
listen 80;
server_name example.com;
rewrite ^(.*)$ https://example.com$1 permanent;
}
rewrite 会重新走一遍 URI 匹配流程,占用一次正则引擎的处理,QPS 高时会白白消耗 CPU,同时写死域名导致多域名无法通用。除非你的 Nginx 版本过老不支持 return,否则一律用写法一。
写法三:同一 server 块内按 $scheme 判断(适合 80/443 合并的简单站点)
server {
listen 80;
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
# 站点主体配置
}
Nginx 官方对 if 的使用极其谨慎,唯二建议的场景就是"if + return"和"if + rewrite",这里恰好安全。不过 80 与 443 合并在一个块里,存在 HTTP/2 与证书选择上的隐性限制,生产环境仍建议拆开。
写法四:default_server 兜底跳转(泛域名/未匹配域名统一处理)
一台服务器挂很多域名时,总会有请求打到某个没配置的域名上。用 default_server 兜底,既能统一跳转,也能避免暴露默认站点内容。
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
这里 server_name _; 表示匹配所有未命名的域名,配合 $host 可以做到"谁来都跳到自己对应的 HTTPS 域名"。如果服务器只有一个证书,也可改成 return 301 https://example.com$request_uri; 全部汇聚到主域名。
三、HSTS:让浏览器替你记住"只走 HTTPS"
强制跳转的短板在于:第一次访问时浏览器仍然会先发起一次 80 端口的请求(哪怕只为了收一个 301),跳转本身需要一次额外往返,且仍存在被劫持的窗口。HSTS(HTTP Strict Transport Security)解决这个问题——服务器通过响应头告诉浏览器:未来 N 秒内,本站只允许 HTTPS,浏览器自动把 HTTP 请求改写成 HTTPS。
server {
listen 443 ssl http2;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# 其余配置
}
配置要点:
- 该响应头只在 HTTPS 响应中才会被浏览器采纳,所以必须加在 443 的 server 块里,加在 80 块里是无效的。
always参数保证 404、502 等错误响应也携带该头,避免"主页面有 HSTS、错误页没有"导致行为不一致。includeSubDomains会覆盖所有子域名,如果你的子域名里有仍走 HTTP 的内部系统,先别加这一项。- 启用
preload后需要到 https://hstspreload.org 提交域名,浏览器硬编码强制 HTTPS,撤掉配置也无法立刻生效——上线前务必确认证书有效期和续期方案,否则站点会被"锁死"在失效的 HTTPS 上。
四、宝塔面板、Apache、Caddy 场景的跳转配置
宝塔面板
在网站设置 → SSL 中开启"强制 HTTPS"即可,面板会在对应 Nginx 站点配置里自动生成如下片段,原理与写法一完全一致:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
如果开启后发现某些接口(如支付回调、微信回调)被强制跳转导致失败,需要到配置文件里删除该 server 块,单独为回调路径保留 80 端口访问。
Apache(.htaccess 或 vhost 配置)
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]
或者更简单的 Redirect 指令(适用于无路径改写需求):
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
Caddy 2(零配置自动 HTTPS)
Caddy 只要配置了域名就会自动申请证书,且默认把 HTTP 请求以 308 重定向到 HTTPS。如需显式控制跳转,可以这样写:
example.com {
redir https://{host}{uri} permanent
}
Caddy 默认用 308(语义等价于 307,保留请求方法),如需 SEO 合并权重可以改成 permanent 的 301。
五、反代与 CDN 场景:循环重定向与 X-Forwarded-Proto
很多报错"重定向次数过多"都出在这一节。根因是:客户端走 HTTPS,但 Nginx 反代到后端时变成 HTTP,后端应用发现不是 HTTPS 又下发一次跳转,如此往返,浏览器直接给出 ERR_TOO_MANY_REDIRECTS。
反代场景:透传 X-Forwarded-Proto
前端 Nginx 负责终结 TLS,后端只需要知道"客户端原来是用什么协议访问的"。此时必须在 location 里带上透传头:
server {
listen 443 ssl http2;
server_name api.example.com;
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-Forwarded-For $proxy_add_x_forwarded_for;
}
}
后端(如 Spring Boot、Django、php-fpm)需要信任该头,否则仍然认为自己是 HTTP。以常见框架为例:
# Spring Boot
server.forward-headers-strategy=framework
# Django(settings.py)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CDN 场景:回源是 HTTP,别让源站再跳一次
CDN 加速的典型拓扑是"用户 → CDN(HTTPS) → 源站(HTTP 回源)"。此时源站对 CDN 的回源请求绝不能返回 301,否则 CDN 跟着重定向,形成"用户永远打不开"的循环。正确的源站写法是:只有真正的 HTTP 客户端才跳转,CDN 转发来的回源请求直接放行。
server {
listen 80;
server_name example.com;
# 只要不是 https(无论是直连还是经 CDN 回源),才跳转
if ($http_x_forwarded_proto != "https") {
return 301 https://$host$request_uri;
}
# 到达这里说明是 CDN 的 https 回源,正常处理站点内容
location / {
proxy_pass http://127.0.0.1:8080;
}
}
同时要在 CDN 控制台确认"回源跟随重定向"默认关闭,且回源 Host 与证书域名一致,否则会命中源站的其他虚拟主机。
六、常见报错与排错命令清单
1. 循环重定向 ERR_TOO_MANY_REDIRECTS
# 查看重定向链,最后一跳是不是又回到 http curl -sIL http://example.com | grep -iE "^(HTTP|Location)" # 模拟 CDN 回源请求,验证源站是否错误跳转 curl -sI -H "X-Forwarded-Proto: https" http://源站IP/ -H "Host: example.com"
修复思路:按第五节检查反代透传头与后端信任配置、检查 CDN 回源协议与"跟随重定向"开关、检查应用层是否自己又发了一轮跳转。
2. ERR_SSL_PROTOCOL_ERROR / 证书不匹配
# 查看证书实际签发域名与有效期 echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -issuer -dates # 验证 SNI 是否正常(多证书服务器常用) openssl s_client -connect example.com:443 -servername example.com /dev/null | grep -i "subject="
常见原因:跳转目标域名没有对应证书、多个虚拟主机共用 443 时 SNI 配置缺失、服务器时间漂移导致证书校验失败(可 timedatectl set-ntp true 校准)。
3. 改了配置不生效 / 语法报错
# 检查语法,错误会精确到文件和行号 nginx -t # 确认无误后平滑重载 systemctl reload nginx # 查看错误日志,跳转问题常伴随 rewrite/upstream 报错 tail -f /var/log/nginx/error.log
4. 验证 HSTS 是否真的生效
curl -sI https://example.com | grep -i "strict-transport-security" # 期望输出:strict-transport-security: max-age=31536000; includeSubDomains; preload
如果期望头缺失:检查是否加在 443 块、是否带了 always、是否被同名的 add_header 覆盖(Nginx 中 add_header 继承规则:下层 location 一旦出现 add_header,会整体覆盖上层,需要重复声明)。
5. 跳转后丢失参数或路径
确认跳转语句用的是 $host$request_uri 而不是 $host,$request_uri 包含原始 URI 与全部查询参数;如果用 rewrite 方式,检查正则是否吞掉了斜杠后的内容。
七、验收清单与小结
- 80 端口返回 301,且 Location 为
https://$host$request_uri,参数完整。 - 443 端口直接访问返回 200,证书链完整、域名匹配。
- HSTS 头出现在所有 HTTPS 响应(含错误页),且不打算撤站时再考虑 preload。
- 反代后端能看到正确的
X-Forwarded-Proto: https,应用层不再二次跳转。 - CDN 回源不再触发 301 循环,各区域测速节点访问正常。
- 用 curl 全链路跑一遍,确认无缓存污染的 301 残留。
强制 HTTPS 跳转本身并不复杂,绝大多数线上事故都出在"反代/CDN 多层链路里协议感知不一致"。把 301 的选型、HSTS 的作用域、X-Forwarded-Proto 的透传这三件事想清楚,再配合上面的命令逐层验证,就能稳定地让全站只走加密通道。


