如何使用Django/nginx部署仅支持HTTPS的网站?

45
我的初始问题是如何为Django登录页面启用HTTPS,唯一的回答建议我-将整个站点设为仅限HTTPS。 考虑到我正在使用Django 1.3和nginx,正确的使网站仅限HTTPS的方法是什么?
一个回答提到了中间件解决方案,但有以下警告:

Django无法在保留POST数据的同时执行SSL重定向。 请设置视图,以便重定向仅在GET期间发生。

Server Fault上关于nginx重写为https的问题,也提到了POST丢失数据的问题,而我对nginx不够熟悉,无法确定解决方案的有效性。 EFF推荐使用HTTPS-only的说明如下:

应用程序在设置cookie时必须设置Secure属性。此属性指示浏览器仅通过安全(HTTPS)传输发送cookie,永远不会通过不安全的(HTTP)传输发送cookie。

像Django-auth这样的应用程序是否具有设置cookie为Secure的功能?还是我需要编写更多的中间件?
那么,从以下方面来看,配置Django / nginx组合实现仅限HTTPS的最佳方法是什么?
  • 安全性
  • 保留POST数据
  • 正确处理cookies
  • 与其他Django应用程序(例如Django-Auth)的交互正常工作
  • 我不知道的任何其他问题 :)

编辑 - 我刚刚在测试多个浏览器时发现另一个问题。假设我有URL https://mysite.com/search/,其中包含搜索表单/按钮。我点击按钮,像往常一样在Django中处理表单,并使用Django HttpResponseRedirect重定向到http://mysite.com/search?results="foo"。Nginx将其重定向到所需的https://mysite.com/search?results="foo"

然而 - 当重定向发生时,Opera会出现可见的闪烁。即使对于相同的搜索词,每次搜索都会发生这种情况(我想https确实不会缓存:) 更糟的是,当我在IE中测试它时,我首先收到以下消息:

您即将被重定向到一个不安全的连接 - 是否继续?

点击“是”后,立即出现以下内容:

您即将查看通过安全连接的页面 - 是否继续?

尽管第二个IE警告有一个关闭选项 - 但第一个警告没有,因此每当有人搜索并被重定向到结果页面时,他们至少会收到一条警告消息。

3个回答

65
对于John C的回答第二部分和Django 1.4+版本...
你可以将request.scheme更改为https,而不是扩展HttpResponseRedirect。 因为Django位于Nginx反向代理后面,它不知道原始请求是否安全。
在Django设置中,设置SECURE_PROXY_SSL_HEADER属性:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')

然后,你需要在反向代理中设置自定义头部,使用Nginx。在Nginx站点设置中:

location / {
    # ... 
    proxy_set_header X-Forwarded-Proto $scheme;
}

这样,request.scheme == 'https'request.is_secure()会返回True。request.build_absolute_uri()会返回https://...等等...


1
Django的文档似乎建议使用HTTP_X_FORWARDED_PROTO而不是HTTP_X_FORWARDED_PROTOCOL,这会有影响吗? - meshy
1
只要在nginx和Django设置中设置相同的标题(nginx中的X-Forwarded-Proto),任何名称都可以使用(包括proto)。当我写这个时,我不知道“proto”是惯例。 - yprez
1
我已经更新了答案以符合文档,感谢您指出。 - yprez
对于任何使用nginx和uwsgi提供django服务的人。请注意,在这种情况下,django已经知道请求是否安全,因此您不需要指定“SECURE_PROXY_SSL_HEADER”。is_secure()检查scheme属性,该属性调用_get_scheme(self),而WSGIRequest应该实现_get_scheme(self),它执行return self.environ.get('wsgi.url_scheme')。并且 - 您猜对了 - uwsgi相应地填充了此环境变量。 - PF4Public
如果您正在使用AWS ALB/ELB来处理http/https重定向,并且将http请求发送到您的nginx容器,则请将标头设置为proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; - Bossam

21

这是我目前想出的解决方案。它由两部分组成:配置nginx和编写Django代码。 nginx部分处理外部请求,将http页面重定向到https,而Django代码处理具有http前缀的内部URL生成(至少是那些由 HttpResponseRedirect() 生成的)。综合起来,它似乎效果很好 - 据我所知,客户端浏览器从未看到用户未输入的http页面。

第一部分,nginx配置

# nginx.conf
# Redirects any requests on port 80 (http) to https:
server {
    listen       80;
    server_name  www.mysite.com mysite.com;
    rewrite ^ https://mysite.com$request_uri? permanent;
#    rewrite ^ https://mysite.com$uri permanent; # also works
}
# django pass-thru via uWSGI, only from https requests:
server {
    listen       443;
    ssl          on;
    ssl_certificate        /etc/ssl/certs/mysite.com.chain.crt;
    ssl_certificate_key    /etc/ssl/private/mysite.com.key;

    server_name  mysite.com;
    location / {
        uwsgi_pass 127.0.0.1:8088;
        include uwsgi_params;
    }
}

第二部分A,来自settings.py的各种安全cookie设置

SERVER_TYPE = "DEV"
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True #目前仅在Django的Dev分支中.
SESSION_EXPIRE_AT_BROWSER_CLOSE = True

第二部分B,Django代码

# mysite.utilities.decorators.py
import settings

def HTTPS_Response(request, URL):
    if settings.SERVER_TYPE == "DEV":
        new_URL = URL
    else:
        absolute_URL = request.build_absolute_uri(URL)
        new_URL = "https%s" % absolute_URL[4:]
    return HttpResponseRedirect(new_URL)

# views.py

def show_items(request):
    if request.method == 'POST':
        newURL = handle_post(request)
        return HTTPS_Response(request, newURL) # replaces HttpResponseRedirect()
    else: # request.method == 'GET'
        theForm = handle_get(request)
    csrfContext = RequestContext(request, {'theForm': theForm,})
    return render_to_response('item-search.html', csrfContext)

def handle_post(request):
    URL = reverse('item-found') # name of view in urls.py
    item = request.REQUEST.get('item')
    full_URL = '%s?item=%s' % (URL, item)
    return full_URL
请注意,将HTTPS_Response()重写为一个装饰器是可能的。 这样做的好处是 - 不需要浏览整个代码并替换HttpResponseRedirect()。 缺点是 - 您需要在django.http.__init__.py的Django中,在HttpResponseRedirect()之前放置这个装饰器。 我不想修改Django的代码,但这取决于您 - 这当然是一种选择。

5

如果将整个网站放在https后面,则无需在Django端担心。 (假设您不需要保护Nginx和Django之间的数据,仅需要在用户和服务器之间保护)


你是说要使用nginx来进行所有的配置吗? - John C
2
你只需要设置nginx仅响应https请求(可能将任何非https重定向到https)。Django可以在http上运行,因为它只与本地主机上的nginx通信。 - second
2
我已经配置了nginx将所有内容重定向到https,但它并没有完全起作用 - 重定向到另一个带有变量的URL,如https://mysite.com/search?results="abc",会丢失这些变量。 - John C

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