Nginx在处理请求时会把大量请求相关的信息暴露成内置变量,$scheme就是其中使用频率很高的一个。它保存着当前请求使用的协议名称,客户端通过HTTP访问时值为http,通过HTTPS访问时值为https。掌握了这个变量的用法,就可以在同一份配置里完成协议判断、强制跳转、Header透传等常见需求,不必为两种协议分别写两套server块。本文将围绕$scheme的取值机制、典型用法和容易踩的坑展开详细说明。

$scheme变量到底是什么,它和$https、$ssl_protocol有什么区别
$scheme是Nginx的内置变量,由core模块提供,取值只有两种可能:http或https。它反映的是客户端与Nginx之间实际建立的连接协议。如果监听端口配置了ssl参数,并且客户端完成了TLS握手,那么$scheme就是https,否则就是http。这个判断是Nginx自动完成的,不需要任何额外配置。
除了$scheme,Nginx还有两个和HTTPS相关的变量容易混淆。一个是$https,它只在连接为HTTPS时返回字符串on,否则为空字符串;另一个是$ssl_protocol,它返回的是具体的TLS协议版本,比如TLSv1.2、TLSv1.3,普通HTTP请求下为空。三者的定位完全不同:$scheme告诉你协议是什么,$https相当于一个布尔开关,$ssl_protocol告诉你TLS版本细节。在做协议判断时,用$scheme最直观,可读性也最好。
还有一个细节需要注意:如果Nginx前面还有CDN或者负载均衡器,客户端到边缘节点是HTTPS,但边缘节点回源到你这台Nginx用的是HTTP,那么$scheme的值就是http,因为变量反映的是与Nginx直连的那一跳。这种场景下应该通过X-Forwarded-Proto请求头来还原真实协议,后面的章节会给出具体写法。
用$scheme实现HTTP强制跳转HTTPS的几种写法
最经典的需求就是把所有HTTP请求301跳转到HTTPS。利用$scheme配合if判断,写法非常清晰:
server {
listen 80;
listen 443 ssl;
server_name example.ipipp.com;
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
if ($scheme = http) {
return 301 https://$host$request_uri;
}
}这种写法把80和443端口合并在同一个server块中维护,证书只需要配置一份,跳转逻辑也只有一处,后续要调整域名或证书时不容易漏改。if ($scheme = http)表示当协议是http时触发301重定向,目标地址用$host和$request_uri拼接,保留了原始域名、路径和查询参数,用户访问体验不受影响。
除了if判断,rewrite指令也能配合$scheme完成跳转。不过更简洁的思路是利用rewite内置的条件判断写成三目形式:
server {
listen 80;
listen 443 ssl;
server_name example.ipipp.com;
ssl_certificate /etc/nginx/ssl/example.crt;
ssl_certificate_key /etc/nginx/ssl/example.key;
# 协议为http时跳转,否则原样处理
rewrite ^ $scheme://$host$request_uri? permanent;
}实际项目中更推荐第一种写法。原因是rewrite的正则处理在性能上略逊于直接return,而且rewrite会重新走一遍URI匹配流程,逻辑复杂时排查问题更困难。return指令则直接结束当前处理阶段,语义明确,官方文档也更鼓励用它来做这种简单重定向。另外要注意301和302的选择:301是永久重定向,浏览器会缓存,302是临时重定向。如果跳转规则还在测试阶段,建议先用302,避免浏览器缓存了错误规则导致后续怎么刷新都不生效。
反向代理场景下如何把真实协议传递给后端
Nginx作为反向代理时,后端应用往往需要知道用户原始请求是HTTP还是HTTPS,比如生成跳转链接、设置Cookie的secure属性等。如果直接proxy_pass,后端看到的协议永远是Nginx与后端之间的协议,通常是http,这时就需要借助$scheme把真实协议传过去:
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}其中X-Forwarded-Proto是事实上的标准头,Spring Boot、Django、Express等主流框架都能识别。Spring Boot只要在配置中开启server.forward-headers-strategy=framework或native,Django只要配置SECURE_PROXY_SSL_HEADER,应用拿到的request.isSecure()就会依据这个头返回正确结果。忘记传这个头是反向代理场景下最常见的问题之一,典型症状是后端生成的链接全部变成http,导致页面出现混合内容警告或者登录后又跳回HTTP。
反过来,如果Nginx前面还有一层负载均衡,真实协议信息在X-Forwarded-Proto头里,Nginx要使用map把头还原到变量中再做判断。map指令只在http块中有效,而且比if更安全,是官方推荐的写法:
http {
map $http_x_forwarded_proto $real_scheme {
default $http_x_forwarded_proto;
'' $scheme;
}
server {
listen 80;
server_name example.ipipp.com;
location / {
proxy_pass http://backend;
proxy_set_header X-Forwarded-Proto $real_scheme;
if ($real_scheme = http) {
return 301 https://$host$request_uri;
}
}
}
}这段配置的逻辑是:如果上游传来了X-Forwarded-Proto头,就采用它的值;没有传(说明客户端直连Nginx),就回退到$scheme。这样无论Nginx处于网络架构的哪一层,协议判断都能得到正确结果。
使用$scheme做if判断的注意事项
Nginx的if指令历来被称为evil,因为它只在rewrite阶段执行,且行为在某些上下文中不符合直觉。不过针对$scheme做纯判断并配合return是可以放心使用的,这属于官方文档明确支持的安全用法。需要避免的是在if内部写proxy_pass、rewrite目标含变量这类操作,容易产生意料之外的结果。
另一个常见误区是在日志中排查协议问题时忘记$scheme可以放进log_format。把协议记录下来,事后分析HTTPS覆盖率会方便很多:
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'scheme=$scheme ssl_proto=$ssl_protocol';
access_log /var/log/nginx/access.log main;这样每条访问日志都会带上协议信息,配合awk统计就能快速算出HTTPS流量占比。最后再强调一点:修改任何与$scheme相关的配置后,务必用nginx -t校验语法再reload,跳转规则出错直接影响全站可用性,测试环境先行验证永远是稳妥的做法。
Nginx配置$scheme变量http跳转https修改时间:2026-09-13 20:22:52