Nginx中不能在location块用$scheme拦截裸HTTP请求,因$scheme是只读变量且HTTP请求根本不会进入HTTPS server块;正确做法是配置独立监听80端口的server块,用return 301跳转HTTPS或return 444拒绝连接。

直接用 $scheme 在 location 块中做“拦截未加密 HTTP 裸请求”是行不通的——因为 $scheme 的值在请求进入 location 匹配阶段前就已确定,且它反映的是当前请求的实际协议(http 或 https),不是用来“判定是否该拦截”的开关。真正要拦截裸 HTTP 请求(比如用户直接访问 http://example.com),关键不在 location,而在 server 级别对非 HTTPS 流量的统一处理。
为什么不能在 location 里靠 $scheme 拦截裸 HTTP
$scheme 是只读变量,表示 Nginx 当前处理该请求所用的协议。如果请求本就是 HTTP,$scheme 就是 http;如果是 HTTPS,就是 https。你无法在 location 中用它“阻止”一个已经到达这里的 HTTP 请求——它早已进来了。更关键的是:裸 HTTP 请求根本不会触发你配置了 ssl on 的 HTTPS server 块,所以压根到不了那个带 location 的上下文。
正确做法:用独立的 HTTP server 块主动重定向或拒绝
标准且可靠的方式,是单独声明一个监听 80 端口的 server,在里面统一处理所有 HTTP 流量:
-
推荐:301 强制跳转 HTTPS(最常用、对 SEO 和用户体验友好)
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
} -
严格拦截:直接返回 403 或 444(适合内部服务、测试环境或安全策略极严场景)
server {
listen 80;
server_name example.com;
return 444; # 关闭连接,不发响应(Nginx 特有)
}
特殊情况:同一端口混用 HTTP/HTTPS(不推荐)
极少数场景(如调试、SNI 兼容性测试)可能需在同一个 listen 指令中启用 SSL 又允许非 SSL 流量(用 ssl on + if ($scheme = http))。但这是反模式:
– Nginx 官方已弃用 ssl on;
– if 在 server 上下文中行为复杂,易出错;
– 无法真正“拦截”,只能返回错误响应,且 TLS 握手失败本身就会暴露问题。
不建议采用,应始终分离 80 和 443 端口的 server 块。
补充:如何识别“裸请求”并增强防护
所谓“裸请求”,通常指:
– Host 头为空或非法(如 IP 直连);
– 无有效 server_name 匹配;
– 使用默认 server(default_server)接收的流量。
你可以结合以下方式加固:
- 为 80 端口的 default_server 显式返回 444 或 403:
server {
listen 80 default_server;
return 444;
} - 在 HTTPS server 中,用
if ($scheme = http)属于冗余判断(不可能成立),但可用if ($host !~ ^example\.com$)防止非法域名 SNI 请求。


















