Nginx 强制 HTTPS 跳转实战:301/302 选型、HSTS 配置与常见排错


<!DOCTYPE html>

Nginx 强制 HTTPS 跳转实战:301/302 选型、HSTS 配置与常见排错

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 的透传这三件事想清楚,再配合上面的命令逐层验证,就能稳定地让全站只走加密通道。


手机扫码阅读

微信或手机浏览器扫一扫,随时随地随心阅读与分享

Fail2ban 实战:SSH 防暴力破解、防火墙联动与常见排错

Web 性能优化实战:HTTP/2 开启、gzip/brotli 压缩与浏览器缓存策略

评 论
更换验证码