Django REST框架分页链接未使用HTTPS

44

我正在为某个DRF端点设置分页,它工作得很好-但是当部署在使用HTTPS的服务器上时,下一页和上一页的链接会形成http://而不是https://。这会导致浏览器阻止请求下一页/上一页。

我已经仔细检查了初始请求是否使用HTTPS,并且这个问题的第二个答案表明,由于请求是通过HTTPS发送的,所以应该在生成的URL中使用HTTPS。

那个问题的第一个答案也没有帮助-我将X-Forwarded-Proto行添加到我的nginx配置中并重新加载,但没有效果。

DRF文档提到reverse()应该像基本的Django reverse一样运行,但很明显初始请求是HTTPS,而返回的URL是HTTP。

这里有几张截图显示了初始请求(https://<domain>.com/api/leaderboard/):

enter image description here

响应包含next: http://<domain>.com/api/leaderboard/?page=2):

enter image description here

我认为这应该是一个简单的设置,但在搜索了这个站点和DRF站点后仍然找不到任何东西。

这是我的nginx配置:

 location / {
    # proxy_pass http://127.0.0.1:9900;
    proxy_set_header X-Forwarded-Host $server_name;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';

    root /opt/app/client/dist;
    index index.html index.htm;

}

这个问题包含了一个相当详细的答案,但最终说道URL使用与请求相同的协议来形成,但这似乎在这里不是这种情况。我需要设置Django SECURE_PROXY_SSL_HEADER吗?鉴于它可能存在安全问题的警告,我不确定。


我应该给这个问题2分。谢谢。 - mahyard
你好,你解决了这个问题吗?我和你遇到了同样的问题,基本URL没问题,但是添加“page=”后它会强制重定向到HTTP。 - Weber Huang
3个回答

31

由于这是在谷歌搜索结果中排名靠前的SO(Stack Overflow)帖子,我认为分享一下我的解决方法会很有用。我的情况与Kubernetes相关,但实质上逻辑与其他情况并没有太大不同。

我正在使用Kubernetes Ingress控制器来管理Django,因此您可能会遇到不同的问题,但我将总结我的情况,以便您判断是否有用:

问题

我遇到了与OP相同的问题,即DRF的分页链接始终为http,即使我使用https访问API端点也是如此。如果您设置了Django REST Framework(DRF)附带的浏览器API并转到api根页面,则可以更清楚地看到这一点。我看到DRF生成的所有链接都是http,而不管我使用何种协议访问网站。

我的环境

从外部世界到Django的顺序:

  1. Kubernetes Nginx Ingress控制器(使用letsencrypt进行SSL设置)
  2. Ingress规则指向Django服务
  3. Gunicorn启动Django服务器
  4. Django读取settings.py并开始提供请求

如何调试并找出原因

  1. 导出ingress controller的nginx.conf文件。如果你不知道如何操作,可以先通过kubectl get pods -n <你的ingress controller所在的namespace>命令查找到ingress controller pod的名称并记下来,然后通过kubectl exec -it -n ingress_controller_namespace ingress_controller_pod_name cat /etc/nginx/nginx.conf > nginx.conf命令导出该文件。
  2. 查看nginx.conf文件,检查是否在你的网站域名的location部分中有proxy_set_header X-Forwarded-Proto $scheme;或类似设置代理头X-Forwarded-Proto的语句。默认情况下,Kubernetes nginx ingress controller应该已经为你添加了这一行(或其等效语句)。
  3. 接下来请求将被发送到Gunicorn。一个需要注意的问题是,你需要向gunicorn命令中添加--forwarded-allow-ips="*"参数,或者如果你知道你的nginx服务器IP,也可以将其限制在该IP上,以便gunicorn能够为你转发标头,否则gunicorn将会去除这些标头。因此,你最终的命令将类似于gunicorn django_server.wsgi:application --forwarded-allow-ips="*" --workers=${PROPER_WORKER_NUM} --log-level info --bind 0.0.0.0:8001。你可以通过指定workers=4来设置4个工作进程,具体取决于服务器上的CPU数量。而--log-level info参数仅用于调试目的,是可选的。
  4. 在Django中,你需要在 settings.py 里添加 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')。这个问题已经被其他答案提到过了。你可能会问为什么要加上前缀 HTTP_,而在nginx中我们的头文件名是 X-Forwarded-Proto。这是因为WSGI将会把它识别出来的头文件加上这个前缀。这行代码基本上是说,如果 HTTP_X_FORWARDED_PROTO 这个头文件等于字符串 "https",那么Django就会认为这是一个安全请求。这会影响Django中的一些行为,例如,你将得到 request.is_secure == Truerequest.build_absolute_uri(None) == 'https://...',最重要的是,Django REST Framework 分页链接现在将使用 https!(只要你确实使用 https 访问api)
  5. 好了,现在你可以再次测试了。如果DRF现在给你提供了https - 恭喜你。或者,如果你和我一样,在尝试了上述方法后,仍然没有成功——DRF仍然生成该死的 http 链接,我想分享一些我调试的小提示:

    在Django中打印以下值,如果你有 DEBUG=True 并且可以访问服务器日志,则使用 print() 打印出来;或者,如果您在启用SSL的生产环境中进行测试,则将它们传递给模板上下文并在html页面中显示它们。

    • request.is_secure(): 如果您无法让DRF使用http,很可能得到的结果是False
    • request.META会给你所有Django接收到的头信息。是否出现了HTTP_X_FORWARDED_PROTO头信息?它的值是什么?
      • 我想分享一下我的“顿悟时刻”: HTTP_X_FORWARDED_PROTO的值是https,https,这告诉我它在某种程度上有重复的值,我可能设置了两次proxy_set_header, Bingo!默认情况下K8 ingress控制器已经有了这行代码,而由于我通过location-snippet再次添加了它,所以我总共有两行proxy_set_header X-Forwarded-Proto $scheme; 。删除我的片段后,DRF显示https,我非常高兴完成了这项工作。这并不是那么直截了当,因为我认为proxy_set_header 将覆盖并“设置”头信息的值。但似乎它只是不断地追加。

嗨,这很有帮助。但是在我的情况下,只需将proxy_set_header X-Forwarded-Proto $scheme;更改为proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;即可解决问题。 我的设置是客户端-> http / https-> ALB-> http-> nginx-> http-> django,其中nginx / django在同一个docker容器中。我认为$scheme只是将传递给nginx的协议转发,而$http_x_forwarded_proto包含通过ALB从客户端传递的原始协议。 希望这能帮助其他人。 - Bossam
1
@Bossam 很高兴你使它工作了!我认为这是因为在你的情况下,你使用的是 AWS ALB,它会为你设置 $http_x_forwarded_proto。在我的设置中,我使用自己的 nginx 作为负载均衡器,而我的 nginx 直接暴露给外部流量。我认为这也解释了为什么你的 nginx proxy_set_header X-Forwarded-Proto $scheme 不起作用,因为你的 AWS ALB 可能将所有外部流量定向到内部的 http 上,所以当你的 nginx 在 ALB 后面时,$schema 始终是 http,因此你永远无法在 Django 中获得 https。 - Shawn
肯定是这样的!感谢您的反馈! - Bossam
2
这是一个非常棒的答案。对我来说,路线是NGINX -> Gunicorn -> Django,所有都在Docker实例中运行。在我的Gunicorn配置中设置 forwarded_allow_ips = '*' 就解决了问题。 - phoenix
1
这个解决方案对我没有用。我仍然有分页链接指向https。 - Micheal J. Roberts
显示剩余2条评论

10

我需要设置Django SECURE_PROXY_SSL_HEADER吗?鉴于它可能不安全的警告,我不确定。

是的,你需要设置。 然而,你需要注意你所做的事情。特别是确保它从外部删除X-Forwarded-Proto。


1
明白了,所以这需要设置 X-Forwarded-Proto 标头以及 Django 的设置吗?这部分 Web 开发/部署对我来说肯定是一个薄弱环节 - 我该如何确保外部的 X-Forwarded-Proto 被删除? - dkhaupt
哦,我已经在我的nginx配置中添加了此设置并保留了“proxy_set_header”行,但它仍然无法工作。我会继续使用这个配置进行测试。 - dkhaupt
@prof.phython,虽然我没有使用大部分的URL并自己添加https,但这很简单。我相信有一些方法可以让DRF在提供的链接中使用https,但是这个问题中没有任何帮助我解决它。 - dkhaupt
@dkhaupt 好的。那么这个链接是你自己创建的吗? - r4v1
是的-这个API被Angular前端使用,所以我只需确保在那段代码中使用的URL包括HTTPS。我没有在后端创建链接。 - dkhaupt
好的。如果我无法让这个工作起来,我想我也会做同样的事情。 - r4v1

0

这个答案可能不是最好的,但它有效,并且没有必要更改nginx和django SECURE_PROXY_SSL_HEADER。

在settings.py中:

MY_PROTOCOL = "https"

在views.py文件中:

from django.conf import settings

class MyView(generics.ListAPIView):
    serializer_class = MySerializer
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated]

    def get(self, request, *args, **kwargs):
        response = super().get(self, request, *args, **kwargs)
        if settings.MY_PROTOCOL == "https":
            if response.data["next"]:
                response.data["next"] = response.data["next"].replace(
                "http://", "https://"
            )
            if response.data["previous"]:
                response.data["previous"] = response.data["previous"].replace(
                "http://", "https://"
            )
        return response

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接