总是得到 http:// 而非 https:// 的原因,以及通过 Nginx 与 Django 两端配置来彻底解决该问题的方案。


摘要

在 Nginx 反向代理环境下,Django 默认 无法 识别客户端与代理之间的 HTTPS 连接,因而 HttpRequest.scheme 始终返回 "http",导致模板中依赖 {{ request.scheme }} 构建的 URL 默认使用 HTTP 协议。本文通过两步配置——在 Nginx 中传递 X-Forwarded-Proto 头部,以及在 Django settings.py 中设置 SECURE_PROXY_SSL_HEADER,使 Django 将该头部映射为 HTTPS 请求,从而正确生成以 https:// 开头的链接。


问题描述

<li>
  <i class="iconfont icon-image mr-2"></i>
  Logo:
  <a href="{% static 'avatar.jpg' %}" target="_blank">
    {{ request.scheme }}://{{ request.get_host }}{% static 'avatar.jpg' %}
  </a>
</li>
  • 现象:即使浏览器地址栏已显示 https://yys.zone,模板渲染出的链接仍是 http://yys.zone/static/avatar.jpg
  • 根本原因
    • Django 的 HttpRequest.scheme 属性依据 request.is_secure() 来判断当前请求是否为 HTTPS,但该判断依赖于 WsgiRequest.META['HTTP_X_FORWARDED_PROTO'](或类似头部)指示的协议类型,而默认情况下并未传递该头部。
    • Nginx 与 Django 间的通信多为 HTTP,Nginx 未告知后端当前连接为 HTTPS,导致 request.is_secure() 返回 False,进而 request.scheme == 'http'

解决方案

1. 在 Nginx 配置中传递 X-Forwarded-Proto

在你的反向代理配置(如 /etc/nginx/conf.d/yys.conf)中,将 location / 段修改为:

location / {
    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;
    proxy_redirect         off;
    proxy_pass             http://127.0.0.1:8000;
}
  • proxy_set_header X-Forwarded-Proto $scheme; 会将客户端请求的协议(http 或 https)传递给后端。
  • proxy_redirect off; 可防止 Nginx 修改后端返回的重定向 URL,确保 Django 的重定向不被干扰。

补充:在多级代理场景下,可借助 map 指令保留已有头部并避免覆盖:

map $http_x_forwarded_proto $thescheme {
    default $scheme;
    https   https;
}
proxy_set_header X-Forwarded-Proto $thescheme;

该做法可在保留原有 X-Forwarded-Proto 的基础上追加或回退至正确协议。


2. 在 Django 中启用 SECURE_PROXY_SSL_HEADER

在 Django 项目的 settings.py 中添加:

# 告诉 Django 信任由上游代理设置的 X-Forwarded-Proto 头部
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
  • 该设置会使当 request.META['HTTP_X_FORWARDED_PROTO'] == 'https' 时,request.is_secure() 返回 True,从而 request.scheme == 'https'

验证方法

  1. 浏览器测试
    • 访问 https://yys.zone,点击页面中的“查看头像”链接,确认 URL 已为 https://yys.zone/static/avatar.jpg
  2. 开发者工具
    • 打开 Network 面板,查看请求头部中的 X-Forwarded-Proto,应为 https
  3. 后端调试
    def debug_view(request):
        print(request.is_secure(), request.scheme)
        # True https
        ...
    如果输出 True, 'https',说明配置生效。

配置示例汇总

# HTTP 重定向到 HTTPS(方案 A)
server {
    listen 80;
    server_name yys.zone www.yys.zone;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location /static {
        alias /home/blog/static;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

# HTTPS 主服务
server {
    listen 443 ssl;
    server_name yys.zone www.yys.zone;

    ssl_certificate     /etc/letsencrypt/live/yys.zone/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yys.zone/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;

    location /static {
        alias /home/blog/static;
    }

    location / {
        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;
        proxy_redirect         off;
        proxy_pass             http://127.0.0.1:8000;
    }
}
# settings.py
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

通过上述配置,Django 在生成模板链接时,{{ request.scheme }} 将正确输出 https,从而避免在 HTTPS 环境下生成不安全的 HTTP 链接。若有更多部署或安全相关问题,欢迎进一步交流!