使用 Gunicorn、Nginx 和 HTTPS 安全地部署 Django 应用程序

举报
Yuchuan 发表于 2021/11/24 19:24:35 2021/11/24
【摘要】 将Django应用程序从开发到生产是一个艰巨但有益的过程。本教程将带您逐步完成该过程,提供深入的指南,从简单的 Django 应用程序开始,并添加Gunicorn、Nginx、域注册和以安全为中心的HTTP 标头。阅读本教程后,您将能够更好地将 Django 应用程序投入生产并向全世界提供。

目录

Django应用程序从开发到生产是一个艰巨但有益的过程。本教程将带您逐步完成该过程,提供深入的指南,从简单的 Django 应用程序开始,并添加GunicornNginx域注册和以安全为中心的HTTP 标头。阅读本教程后,您将能够更好地将 Django 应用程序投入生产并向全世界提供。

在本教程中,您将学习

  • 如何将 Django 应用程序从开发带到生产
  • 如何在现实世界的公共领域托管您的应用程序
  • 如何将GunicornNginx引入请求和响应链
  • HTTP 标头如何加强您网站的 HTTPS 安全性

为了充分利用本教程,您应该对 PythonDjango和 HTTP 请求的高级机制有一个入门级的理解

您可以通过以下链接下载本教程中使用的 Django 项目:

从 Django 和 WSGIServer 开始

您将使用Django作为 Web 应用程序核心的框架,将其用于 URL 路由、HTML 呈现、身份验证、管理和后端逻辑。在本教程中,您将用另外两个层GunicornNginx补充 Django 组件,以便可扩展地为应用程序提供服务。但在此之前,您需要设置环境并使 Django 应用程序本身启动并运行。

设置云虚拟机 (VM)

首先,您需要启动并设置一个虚拟机 (VM),Web 应用程序将在该虚拟机 (VM)上运行。您应该至少熟悉一个基础架构即服务 (IaaS)云服务提供商来配置 VM。本节将引导您完成整个过程,但不会详细介绍每个步骤。

使用 VM 为 Web 应用程序提供服务是 IaaS 的一个示例,您可以在其中完全控制服务器软件。除了 IaaS 之外,其他选项确实存在:

  • 一个无服务器架构允许您只组成Django应用程序,让一个单独的框架或云提供商处理基础设施方面。
  • 一个集装箱方法允许多个应用程序在同一主机操作系统上独立运行。

但是,在本教程中,您将使用直接在 IaaS 上为 Nginx 和 Django 提供服务的行之有效的途径。

两个流行的虚拟机选项是腾讯 VM阿里 EC2。要获得有关启动实例的更多帮助,您应该参考云提供商的文档:

Django 项目和本教程中涉及的所有其他内容都位于运行 Ubuntu Server 20.04的t2.micro 阿里 EC2 实例上。

VM 设置的一个重要组成部分是入站安全规则。这些是控制到您的实例的入站流量的细粒度规则。为初始开发创建以下入站安全规则,您将在生产中修改这些规则:

Reference Type Protocol Port Range Source
1 Custom TCP 8000 my-laptop-ip-address/32
2 Custom All All security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

现在,您将一次浏览这些内容:

  1. 规则 1允许 TCP 通过端口 8000 来自您个人计算机的 IPv4 地址,允许您在开发过程中通过端口 8000 向 Django 应用程序发送请求。
  2. 规则 2允许来自分配给同一安全组的网络接口和实例的入站流量,使用安全组 ID 作为源。这是您应该绑定到您的实例的默认 AWS 安全组中包含的规则。
  3. 规则 3允许您从您的个人计算机通过 SSH 访问您的 VM。

您还需要添加出站规则以允许出站流量执行诸如安装包之类的操作:

Type Protocol Port Range Source
Custom All All 0.0.0.0/0

综合起来,您的初始 AWS 安全规则集可以包含三个入站规则和一个出站规则。这些又来自三个独立的安全组——默认组、一个用于 HTTP 访问的组和一个用于 SSH 访问的组:

Django 应用程序的初始安全规则集

然后,您可以从本地计算机通过SSH 连接到实例:

$ ssh -i ~/.ssh/<privkey>.pem ubuntu@<instance-public-ip-address>

此命令将您以用户身份登录到您的 VM ubuntu。这里~/.ssh/<privkey>.pem私钥的路径,它是您绑定到 VM 的一组安全凭证的一部分。VM 是 Django 应用程序代码所在的位置。

有了这个,您应该已经准备好继续构建您的应用程序了。

创建一个 Cookie-Cutter Django 应用程序

在本教程中,您不关心使用复杂的 URL 路由或高级数据库功能制作一个花哨的 Django 项目。相反,您需要简单、小巧且易于理解的东西,让您可以快速测试您的基础架构是否正常工作。

为此,您可以执行以下步骤来设置您的应用程序。

首先,通过 SSH 连接到您的 VM 并确保您安装了 Python 3.8 和 SQLite3 的最新补丁版本:

$ sudo apt-get update -y
$ sudo apt-get install -y python3.8 python3.8-venv sqlite3
$ python3 -V
Python 3.8.10

这里,Python 3.8 是系统 Python,或者是python3Ubuntu 20.04 (Focal) 附带的版本。升级发行版可确保您收到来自最新 Python 3.8.x 版本的错误和安全修复。或者,您可以完全安装另一个 Python 版本 - 例如python3.9- 与系统范围的解释器一起安装,您需要将其显式调用为python3.9.

接下来,创建并激活一个虚拟环境

$ cd  # Change directory to home directory
$ python3 -m venv env
$ source env/bin/activate

现在,安装 Django 3.2:

$ python -m pip install -U pip 'django==3.2.*'

您现在可以使用 Django 的管理命令引导 Django 项目和应用程序:

$ mkdir django-gunicorn-nginx/
$ django-admin startproject project django-gunicorn-nginx/
$ cd django-gunicorn-nginx/
$ django-admin startapp myapp
$ python manage.py migrate
$ mkdir -pv myapp/templates/myapp/

这会在myapp名为 的项目旁边创建 Django 应用程序project

/home/ubuntu/
│
├── django-gunicorn-nginx/
│    │
│    ├── myapp/
│    │   ├── admin.py
│    │   ├── apps.py
│    │   ├── __init__.py
│    │   ├── migrations/
│    │   │   └── __init__.py
│    │   ├── models.py
│    │   ├── templates/
│    │   │   └── myapp/
│    │   ├── tests.py
│    │   └── views.py
│    │
│    ├── project/
│    │   ├── asgi.py
│    │   ├── __init__.py
│    │   ├── settings.py
│    │   ├── urls.py
│    │   └── wsgi.py
|    |
│    ├── db.sqlite3
│    └── manage.py
│
└── env/  ← Virtual environment

使用诸如VimGNU nano 之类的终端编辑器,打开project/settings.py您的应用程序并将其附加到INSTALLED_APPS

# project/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "myapp",
]

接下来,打开myapp/templates/myapp/home.html并创建一个简短的 HTML 页面:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

之后,编辑myapp/views.py以呈现该 HTML 页面:

from django.shortcuts import render

def index(request):
    return render(request, "myapp/home.html")

现在创建并打开myapp/urls.py以将您的视图与 URL 模式相关联:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
]

之后,相应地编辑project/urls.py

from django.urls import include, path

urlpatterns = [
    path("myapp/", include("myapp.urls")),
    path("", include("myapp.urls")),
]

在此期间,您还可以做一件事,即确保用于加密签名的 Django密钥不是硬编码的settings.py,Git 可能会跟踪它。从 中删除以下行project/settings.py

SECRET_KEY = "django-insecure-o6w@a46mx..."  # Remove this line

将其替换为以下内容:

import os

# ...

try:
    SECRET_KEY = os.environ["SECRET_KEY"]
except KeyError as e:
    raise RuntimeError("Could not find a SECRET_KEY in environment") from e

这告诉 Django 在您的环境中查找SECRET_KEY而不是将其包含在您的应用程序源代码中。

注意:对于较大的项目,请查看django-environ使用环境变量配置 Django 应用程序。

最后,在您的环境中设置密钥。以下是在 Ubuntu Linux 上使用OpenSSL将密钥设置为八十个字符的字符串的方法:

$ echo "export SECRET_KEY='$(openssl rand -hex 40)'" > .DJANGO_SECRET_KEY
$ source .DJANGO_SECRET_KEY

您可以cat的内容.DJANGO_SECRET_KEY看到,openssl已经产生了一个加密的安全十六进制字符串键值:

$ cat .DJANGO_SECRET_KEY
export SECRET_KEY='26a2d2ccaf9ef850...'

好了,你已经准备好了。这就是拥有最低限度功能的 Django 应用程序所需的全部内容。

在开发中使用 Django 的 WSGIServer

在本节中,您将使用 来测试 Django 的开发 Web 服务器httpie,这是一个很棒的命令行 HTTP 客户端,用于测试从控制台发送到您的 Web 应用程序的请求:

$ pwd
/home/ubuntu
$ source env/bin/activate
$ python -m pip install httpie

您可以创建一个别名,让您向应用程序发送GET请求httpie

$ # Send GET request and follow 30x Location redirects
$ alias GET='http --follow --timeout 6'

这别名GEThttp具有一些默认标志的调用。您现在可以使用GET docs.python.org从 Python 文档的主页查看响应标头和正文。

在启动 Django 开发服务器之前,您可以检查您的 Django 项目是否存在潜在问题:

$ cd django-gunicorn-nginx/
$ python manage.py check
System check identified no issues (0 silenced).

如果您的检查没有发现任何问题,那么告诉 Django 的内置应用程序服务器开始侦听本地主机,使用默认端口 8000:

$ # Listen on 127.0.0.1:8000 in the background
$ nohup python manage.py runserver &
$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &

使用在后台nohup <command> &执行command,以便您可以继续使用您的 shell。您可以使用jobs -l查看进程标识符 (PID),它可以让您将进程带到前台或终止它。nohup标准输出 (stdout)标准错误 (stderr)重定向到文件nohup.out.

注意:如果它出现nohup挂起并让您没有光标,请按Enter以恢复终端光标和 shell 提示。

runserver反过来,Django 的命令使用以下语法:

$ python manage.py runserver [address:port]

如果address:port如上所述未指定参数,Django 将默认监听localhost:8000. 您还可以使用该lsof命令更直接地验证是否python调用了某个命令来侦听端口 8000:

$ sudo lsof -n -P -i TCP:8000 -s TCP:LISTEN
COMMAND   PID   USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python  43689 ubuntu    4u  IPv4  45944      0t0  TCP 127.0.0.1:8000 (LISTEN)

在本教程的这一点上,您的应用程序仅侦听localhost,即地址127.0.0.1。它尚不能从浏览器访问,但您仍然可以通过GET从 VM 本身的命令行向它发送请求来为其提供第一个访问者:

$ GET :8000/myapp/
HTTP/1.1 200 OK
Content-Length: 182
Content-Type: text/html; charset=utf-8
Date: Sat, 25 Sep 2021 00:11:38 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.10
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

标头Server: WSGIServer/0.2 CPython/3.8.10描述了生成响应的软件。在这种情况下,它的0.2版本WSGIServer一起CPython的3.8.10

WSGIServer无非是 Django 定义的一个 Python 类它实现了 Python WSGI 协议。这意味着它遵循Web 服务器网关接口 (WSGI),该标准定义了Web 服务器软件Web 应用程序交互的方式。

到目前为止,在我们的示例中,django-gunicorn-nginx/项目是 Web 应用程序。由于您正在为开发中的应用程序提供服务,因此实际上没有单独的 Web 服务器。Django 使用simple_server实现轻量级 HTTP 服务器的模块,并将 Web 服务器与应用服务器的概念融合到一个命令中,runserver.

接下来,您将了解如何通过将您的应用与现实世界的域相关联来开始向其介绍您的应用。

使用 Django、Gunicorn 和 Nginx 将您的网站放到网上

此时,您的站点可以在您的 VM 上本地访问。如果您希望通过真实的 URL 访问您的站点,则需要声明一个域名并将其绑定到 Web 服务器。这对于启用 HTTPS 也是必要的,因为某些证书颁发机构不会为您不拥有的裸 IP 地址或子域颁发证书。在本节中,您将看到如何注册和配置域。

设置静态公共 IP 地址

如果您可以将域的配置指向一个保证不会更改的公共 IP 地址,那就再理想不过了。云虚拟机的一个次优属性是,如果实例处于停止状态,它们的公共 IP 地址可能会更改。或者,如果您出于某种原因需要用新实例替换现有 VM,则由此产生的 IP 地址更改将是有问题的。

这种困境的解决方案是将静态 IP 地址绑定到实例:

按照您的云提供商的文档将静态 IP 地址与您的云 VM 相关联。在用于本教程示例的 AWS 环境中,弹性 IP 地址50.19.125.152与 EC2 实例相关联。

注意:请记住,这意味着您需要更改目标 IPssh才能通过 SSH 连接到您的 VM:

$ ssh [args] my-new-static-public-ip

更新目标 IP 后,您将能够连接到云 VM。

在您的 VM 前使用更稳定的公共 IP,您就可以链接到域了。

链接到域

在本节中,您将逐步了解如何购买、设置域名并将其链接到现有应用程序。

这些示例使用Namecheap,但请不要将其视为明确的认可。还有很多其他选项,例如domain.comGoDaddyGoogle Domains。就偏向性而言,Namecheap 为成为本教程中的首选域注册商而支付了 0 美元。

警告:如果您想在公共域上为您的站点提供服务并DEBUG设置为True,您需要创建自定义入站安全规则以仅允许您的个人计算机和 VM 的 IP 地址。你应该打开任何HTTP或HTTPS入站规则,0.0.0.0直到你关闭DEBUG至少是。

您可以通过以下方式开始:

  1. Namecheap创建一个帐户,确保设置双因素身份验证 (2FA)。
  2. 从主页开始搜索适合您预算的域名。您会发现价格可能因顶级域 (TLD) 和主机名而异。
  3. 当您对选择感到满意时购买域

本教程使用域supersecure.codes,但您将拥有自己的域。

注意:在您阅读本教程时,请记住这supersecure.codes只是一个示例域,并没有得到积极维护。

在选择您自己的域时,请记住,选择更深奥的站点名称和顶级域 (TLD) 通常会导致购买该域的标价更便宜。这对于测试目的尤其有用。

一旦你有你的域名,你要打开WithheldForPrivacy保护,正式称为WhoisGuard。当有人whois在您的域上进行搜索时,这将掩盖您的个人信息。以下是如何执行此操作:

  1. 选择帐户 → 域列表
  2. 选择您的域旁边的管理
  3. 启用WithheldForPrivacy保护。

接下来,是时候为您的站点设置 DNS 记录表了。每个 DNS 记录将成为数据库中的一行,告诉浏览器完全限定域名 (FQDN)指向的底层 IP 地址。在这种情况下,我们想要supersecure.codes路由到 50.19.125.152,VM 可以到达的公共 IPv4 地址:

  1. 选择帐户 → 域列表
  2. 选择您的域旁边的管理
  3. 选择高级 DNS
  4. Host Records 下,为您的域添加两个A 记录

如下添加 A 记录,替换50.19.125.152为您实例的公有 IPv4 地址:

Type Host Value TTL
A Record @ 50.19.125.152 Automatic
A Record www 50.19.125.152 Automatic

一个A记录可以让你一个域名或子站点,你为你的应用程序的Web服务器的IPv4地址相关联。在上面,字段应使用您的 VM 实例的公共 IPv4 地址。

您可以看到Host字段有两种变体:

  1. 在这种情况下,使用@指向根域supersecure.codes
  2. Usingwww意味着www.supersecure.codes将指向与 just 相同的位置supersecure.codes。从技术上讲,这www一个子域,可以将用户发送到与较短的supersecure.codes.

设置 DNS 主机记录表后,您最多需要等待 30 分钟,路由才会生效。您现在可以终止现有runserver进程:

$ jobs -l
[1]+ 43689 Running                 nohup python manage.py runserver &
$ kill 43689
[1]+  Done                    nohup python manage.py runserver

您可以pgrep通过再次检查活动作业来确认该过程已经结束:

$ pgrep runserver  # Empty
$ jobs -l  # Empty or 'Done'
$ sudo lsof -n -P -i TCP:8000 -s TCP:LISTEN  # Empty
$ rm nohup.out

有了这些东西,您还需要调整 Django 设置ALLOWED_HOSTS,它是您让 Django 应用程序提供服务的域名集:

# project/settings.py
# Replace 'supersecure.codes' with your domain
ALLOWED_HOSTS = [".supersecure.codes"]

前导点 ( .) 是子域通配符,允许www.supersecure.codessupersecure.codes。保持此列表紧密以防止 HTTP主机标头攻击

现在你可以通过一个轻微的改变来重新启动 WSGIServer:

$ nohup python manage.py runserver '0.0.0.0:8000' &

注意address:port参数 now 0.0.0.0:8000,而之前没有指定:

  • 指定 noaddress:port意味着在 上提供应用程序localhost:8000。这意味着只能从 VM 内部访问该应用程序。您可以通过httpie从相同的 IP 地址调用来与它通话,但是您无法从外部世界访问您的应用程序。

  • 指定一个address:portof'0.0.0.0:8000'使您的服务器对外界可见,但默认情况下仍然在端口 8000 上。该0.0.0.0简写为“绑定到所有IP地址,此计算机支持。” 对于带有一个名为 的网络接口控制器 (NIC)的开箱即用云 VM eth0, using0.0.0.0充当机器的公共 IPv4 地址的替代品。

接下来,打开输出nohup.out以查看来自 Django 的 WSGIServer 的任何传入日志:

$ tail -f nohup.out

现在是真相的时刻。是时候让您的网站成为第一位访问者了。在您的个人计算机上,在 Web 浏览器中输入以下 URL:

http://www.supersecure.codes:8000/myapp/

将上面的域名替换为您自己的域名。您应该会看到页面快速响应:

现在这是一些甜蜜的 HTML!

由于您之前创建的入站安全规则,您可以访问此 URL,但其他人无法访问。

故障连接故障排除显示隐藏

现在返回到您的 VM 的外壳。在 的连续输出中tail -f nohup.out,您应该看到如下所示的内容:

[<date>] "GET /myapp/ HTTP/1.1" 200 182

恭喜,您刚刚迈出了创建自己网站的里程碑式的第一步!但是,在这里暂停并注意 URL 中嵌入的几个大问题http://www.supersecure.codes:8000/myapp/

  • 该站点仅通过 HTTP 提供服务。如果不启用 HTTPS,如果您想将任何敏感数据从客户端传输到服务器,反之亦然,则您的站点从根本上是不安全的。使用 HTTP 意味着请求和响应以纯文本形式发送。你很快就会解决这个问题。

  • URL 使用非标准端口 8000与标准默认 HTTP 端口号 80。它非常规且有点碍眼,但您还不能使用 80。那是因为端口 80 是特权端口,非 root 用户不能——也不应该——绑定到它。稍后,您将在组合中引入一个工具,允许您的应用程序在端口 80 上可用。

如果您检查您的浏览器,您将看到您的浏览器 URL 栏提示这一点。如果您使用的是 Firefox,则会出现一个红色锁形图标,表示连接是通过 HTTP 而不是 HTTPS:

强调不安全图标的 HTTP 页面

展望未来,您希望使操作合法化。您可以开始通过标准端口 80 为 HTTP 提供服务。更好的是,开始提供 HTTPS (443) 服务并将 HTTP 请求重定向到那里。您很快就会看到如何逐步完成这些步骤。

用 Gunicorn 替换 WSGIServer

您想开始将您的应用程序推向为外部世界做好准备的状态吗?如果是这样,那么您应该用manage.py runserver单独的专用应用程序服务器替换 Django 的内置 WSGIServer,它是 使用的应用程序服务器。但是等一下:WSGIServer 似乎工作得很好。为什么要更换?

要回答这个问题,您可以阅读 Django 文档的内容:

请勿在生产环境中使用此服务器。它没有经过安全审计或性能测试。(这就是它的保留方式。我们的工作是制作 Web 框架,而不是 Web 服务器,因此改进此服务器以使其能够处理生产环境超出了 Django 的范围。)(来源

Django 是一个Web 框架,而不是一个 Web 服务器,它的维护者希望明确区分。在本节中,您将runserverGunicorn替换 Django 的命令。Gunicorn 首先是一个 Python WSGI 应用程序服务器,并且是经过实战考验的服务器:

  • 它快速、优化且专为生产而设计。
  • 它使您可以更精细地控制应用程序服务器本身。
  • 它有更完整和可配置的日志记录。
  • 它已经过充分测试,特别是它作为应用程序服务器的功能。

您可以将 Gunicorn 安装pip到您的虚拟环境中:

$ pwd
/home/ubuntu
$ source env/bin/activate
$ python -m pip install 'gunicorn==20.1.*'

接下来,您需要进行一些级别的配置。Gunicorn 配置文件很酷的一点是,它只需要是有效的 Python 代码,变量名与参数相对应。您可以在一个项目子目录中存储多个 Gunicorn 配置文件:

$ cd ~/django-gunicorn-nginx
$ mkdir -pv config/gunicorn/
mkdir: created directory 'config'
mkdir: created directory 'config/gunicorn/'

接下来,打开一个开发配置文件config/gunicorn/dev.py,并添加以下内容:

"""Gunicorn *development* config file"""

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "project.wsgi:application"
# The granularity of Error log outputs
loglevel = "debug"
# The number of worker processes for handling requests
workers = 2
# The socket to bind
bind = "0.0.0.0:8000"
# Restart workers when code changes (development only!)
reload = True
# Write access and error info to /var/log
accesslog = errorlog = "/var/log/gunicorn/dev.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
pidfile = "/var/run/gunicorn/dev.pid"
# Daemonize the Gunicorn process (detach & enter background)
daemon = True

在启动 Gunicorn 之前,您应该停止该runserver过程。使用jobs找到它,并kill阻止它:

$ jobs -l
[1]+ 26374 Running                 nohup python manage.py runserver &
$ kill 26374
[1]+  Done                    nohup python manage.py runserver

接下来,确保上面 Gunicorn 配置文件中设置的值的日志和 PID 目录存在:

$ sudo mkdir -pv /var/{log,run}/gunicorn/
mkdir: created directory '/var/log/gunicorn/'
mkdir: created directory '/var/run/gunicorn/'
$ sudo chown -cR ubuntu:ubuntu /var/{log,run}/gunicorn/
changed ownership of '/var/log/gunicorn/' from root:root to ubuntu:ubuntu
changed ownership of '/var/run/gunicorn/' from root:root to ubuntu:ubuntu

使用这些命令,您已确保 Gunicorn 存在必要的 PID 和日志目录,并且它们可由ubuntu用户写入。

有了这个,您可以使用-c标志启动 Gunicorn以指向项目根目录中的配置文件:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ source .DJANGO_SECRET_KEY
$ gunicorn -c config/gunicorn/dev.py

gunicorn使用dev.py您在上面指定的开发配置文件在后台运行。和以前一样,您现在可以监视输出文件以查看 Gunicorn 记录的输出:

$ tail -f /var/log/gunicorn/dev.log
[2021-09-27 01:29:50 +0000] [49457] [INFO] Starting gunicorn 20.1.0
[2021-09-27 01:29:50 +0000] [49457] [DEBUG] Arbiter booted
[2021-09-27 01:29:50 +0000] [49457] [INFO] Listening at: http://0.0.0.0:8000 (49457)
[2021-09-27 01:29:50 +0000] [49457] [INFO] Using worker: sync
[2021-09-27 01:29:50 +0000] [49459] [INFO] Booting worker with pid: 49459
[2021-09-27 01:29:50 +0000] [49460] [INFO] Booting worker with pid: 49460
[2021-09-27 01:29:50 +0000] [49457] [DEBUG] 2 workers

现在在浏览器中再次访问您网站的 URL。您仍然需要 8000 端口:

http://www.supersecure.codes:8000/myapp/

再次检查您的 VM 终端。您应该在 Gunicorn 的日志文件中看到如下一行或多行:

67.xx.xx.xx - - [27/Sep/2021:01:30:46 +0000] "GET /myapp/ HTTP/1.1" 200 182

这些行是访问日志,告诉您有关传入请求的信息:

Component Meaning
67.xx.xx.xx User IP address
27/Sep/2021:01:30:46 +0000 Timestamp of request
GET Request method
/myapp/ URL path
HTTP/1.1 Protocol
200 Response status code
182 Response content length

为简洁起见,上面排除的是用户代理,它也可能出现在您的日志中。以下是 macOS 上 Firefox 浏览器的示例:

Mozilla/5.0 (Macintosh; Intel Mac OS X ...) Gecko/20100101 Firefox/92.0

随着 Gunicorn 的启动和聆听,是时候将合法的 Web 服务器引入到等式中了。

合并 Nginx

此时,您已将 Django 的runserver命令替换gunicorn为应用程序服务器。还有一个玩家要添加到请求链中:一个像Nginx这样的网络服务器

等等——你已经添加了 Gunicorn!为什么你需要在图片中添加一些新的东西?之所以会这样,是因为 Nginx 和 Gunicorn 是两个不同的东西,它们是共存的,是一个团队。

Nginx将自己定义为高性能 Web 服务器和反向代理服务器。值得分解它,因为它有助于解释 Nginx 与 Gunicorn 和 Django 的关系。

首先,Nginx 是一个Web 服务器,因为它可以为 Web 用户或客户端提供文件。文件是文字文档:HTML、CSS、PNG、PDF——任你说。在过去,在 Django 等框架出现之前,网站的功能基本上是直接查看文件系统是很常见的。在 URL 路径中,斜杠表示服务器文件系统的有限部分上的目录,您可以请求查看这些目录。

请注意术语的细微差别:

  • Django 是一个网络框架。它使您可以构建核心 Web 应用程序,为网站上的实际内容提供支持。它处理 HTML 呈现、身份验证、管理和后端逻辑。

  • Gunicorn 是一个应用服务器。它将 HTTP 请求转换为 Python 可以理解的内容。Gunicorn 实现了Web 服务器网关接口 (WSGI),它是 Web 服务器软件和 Web 应用程序之间的标准接口。

  • Nginx 是一个网络服务器。它是公共处理程序,更正式地称为反向代理,用于传入请求并扩展到数千个同时连接。

Nginx 作为 Web 服务器的一部分作用是它可以更有效地服务静态文件。这意味着,对于图片等静态内容的请求,可以去掉Django这个中间人,让Nginx直接渲染文件。我们将在本教程的后面部分介绍这一重要步骤。

Nginx 也是一个反向代理服务器,因为它位于外部世界和您的 Gunicorn/Django 应用程序之间。与您可能使用代理发出出站请求的方式相同,您可以使用诸如 Nginx 之类的代理来接收它们:

Nginx 和 Gunicorn 的最终配置

要开始使用 Nginx,请安装它并验证其版本:

$ sudo apt-get install -y 'nginx=1.18.*'
$ nginx -v  # Display version info
nginx version: nginx/1.18.0 (Ubuntu)

然后,您应该将您为端口 8000 设置的入站允许规则更改为端口 80。将入站规则替换为TCP:8000以下内容:

类型 协议 端口范围 来源
HTTP TCP 80 my-laptop-ip-address/32

其他规则,例如 SSH 访问规则,应保持不变。

现在,启动nginx服务并确认其状态为running

$ sudo systemctl start nginx
$ sudo systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
   Loaded: loaded (/lib/systemd/system/nginx.service; enabled; ...
   Active: active (running) since Mon 2021-09-27 01:37:04 UTC; 2min 49s ago
...

现在您可以向一个看起来很熟悉的 URL 发出请求:

http://supersecure.codes/

与您之前的情况相比,这是一个很大的不同。URL 中不再需要端口 8000。相反,端口默认为端口 80,看起来更正常:

欢迎来到 Nginx!

这是 Nginx 的一个友好特性。如果您以零配置启动 Nginx,它会为您提供一个页面,表明它正在侦听。现在尝试/myapp以下 URL 中的页面:

http://supersecure.codes/myapp/

记得替换supersecure.codes成自己的域名。

您应该会看到 404 响应,这没关系:

Nginx 404 页面

这是因为您/myapp通过端口 80请求路径,这是 Nginx 而不是 Gunicorn 正在侦听的地方。此时,您有以下设置:

  • Nginx 正在侦听端口 80。
  • Gunicorn 正在单独侦听端口 8000。

在您指定之前,两者之间没有联系或联系。Nginx 不知道 Gunicorn 和 Django 有一些他们希望世界看到的甜蜜 HTML。这就是它返回404 Not Found响应的原因。您还没有设置它来代理对 Gunicorn 和 Django 的请求:

Nginx 与 Gunicorn 断开连接

您需要为 Nginx 提供一些基本配置,以告诉它将请求路由到 Gunicorn,然后 Gunicorn 会将它们提供给 Django。打开/etc/nginx/sites-available/supersecure并添加以下内容:

server_tokens               off;
access_log                  /var/log/nginx/supersecure.access.log;
error_log                   /var/log/nginx/supersecure.error.log;

# This configuration will be changed to redirect to HTTPS later
server {
  server_name               .supersecure.codes;
  listen                    80;
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }
}

请记住,您需要supersecure将文件名替换为您站点的主机名,并确保将 的server_name值替换为.supersecure.codes您自己的域,并以点为前缀。

注意:您可能需要sudo打开/etc.

这个文件就是Nginx 反向代理配置的“Hello World” 。它告诉 Nginx 如何表现:

  • 在端口 80 上侦听使用主机supersecure.codes及其子域的请求。
  • 将这些请求传递http://localhost:8000Gunicorn 正在监听的地方。

proxy_set_header领域是非常重要的。它确保 Nginx 将Host最终用户发送的HTTP 请求标头传递给 Gunicorn 和 Django。Nginx 将Host: localhost默认使用,忽略Host最终用户浏览器发送的标头字段。

您可以使用nginx configtest以下方法验证您的配置文件:

$ sudo service nginx configtest /etc/nginx/sites-available/supersecure
 * Testing nginx configuration                                  [ OK ]

[ OK ]输出表示配置文件是有效的,并且可以进行解析。

现在您需要将此文件符号链接sites-enabled目录,替换supersecure为您的站点域:

$ cd /etc/nginx/sites-enabled
$ # Note: replace 'supersecure' with your domain
$ sudo ln -s ../sites-available/supersecure .
$ sudo systemctl restart nginx

在使用 向您的站点发出请求之前httpie,您需要再添加一个入站安全规则。添加以下入站规则:

类型 协议 端口范围 来源
HTTP TCP 80 vm-static-ip-address/32

此安全规则允许来自 VM 本身的公共(弹性)IP 地址的入站 HTTP 流量。起初这似乎有点过分,但您需要这样做,因为现在请求将通过公共 Internet 路由,这意味着使用安全组 ID 的自引用规则将不再足够。

现在它使用 Nginx 作为 Web 服务器前端,重新向站点发送请求:

$ GET http://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Mon, 27 Sep 2021 19:54:19 GMT
Referrer-Policy: same-origin
Server: nginx
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p>Now this is some sweet HTML!</p>
  </body>
</html>

既然 Nginx 位于 Django 和 Gunicorn 的前面,这里有一些有趣的输出:

  • Nginx 现在返回Server标头为Server: nginx,表明 Nginx 是新的前端 Web 服务器。设置server_tokens为 的值会off告诉 Nginx 不要发出其确切版本,例如nginx/x.y.z (Ubuntu). 从安全角度来看,这会泄露不必要的信息。
  • Nginxchunked用于Transfer-Encoding标头而不是广告Content-Length
  • Nginx 还要求保持与Connection: keep-alive.

接下来,您将利用 Nginx 的核心功能之一:快速有效地提供静态文件的能力。

直接使用 Nginx 提供静态文件

您现在可以将 Nginx 代理请求发送到您的 Django 应用程序。重要的是,您还可以使用 Nginx直接提供静态文件。如果您有DEBUG = Truein project/settings.py,那么 Django 将呈现文件,但这非常低效且可能不安全。相反,您可以让您的 Web 服务器直接呈现它们。

静态文件的常见示例包括本地 JavaScript、图像和 CSS — 任何不需要 Django 作为等式的一部分来动态呈现响应内容的地方。

首先,从您的项目目录中,创建一个位置来保存和跟踪开发中的 JavaScript 静态文件:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ mkdir -p static/js

现在打开一个新文件static/js/greenlight.js并添加以下 JavaScript:

// Enlarge the #changeme element in green when hovered over
(function () {
    "use strict";
    function enlarge() {
        document.getElementById("changeme").style.color = "green";
        document.getElementById("changeme").style.fontSize = "xx-large";
        return false;
    }
    document.getElementById("changeme").addEventListener("mouseover", enlarge);
}());

如果将鼠标悬停在此 JavaScript 上,它将使文本块以绿色大字体爆炸。是的,这是一些前沿的前端工作!

接下来,将以下配置添加到project/settings.py,更新STATIC_ROOT为您的域名:

STATIC_URL = "/static/"
# Note: Replace 'supersecure.codes' with your domain
STATIC_ROOT = "/var/www/supersecure.codes/static"
STATICFILES_DIRS = [BASE_DIR / "static"]

您告诉 Django 的collectstatic命令在哪里搜索和放置从多个 Django 应用程序(包括 Django 自己的内置应用程序,例如admin.

最后但并非最不重要的是,修改 HTMLmyapp/templates/myapp/home.html以包含您刚刚创建的 JavaScript:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

通过包含/static/js/greenlight.js脚本,<span id="changeme">元素将附加一个事件侦听器。

注意:为了使这个示例简单明了,您将 URL 路径硬编码为 ,greenlight.js而不是使用 Django 的static模板标签。您希望在更大的项目中利用该功能。

下一步是创建一个目录路径,该路径将容纳您的项目的静态内容,以便 Nginx 提供服务:

$ sudo mkdir -pv /var/www/supersecure.codes/static/
mkdir: created directory '/var/www/supersecure.codes'
mkdir: created directory '/var/www/supersecure.codes/static/'
$ sudo chown -cR ubuntu:ubuntu /var/www/supersecure.codes/
changed ownership of '/var/www/supersecure.codes/static' ... to ubuntu:ubuntu
changed ownership of '/var/www/supersecure.codes/' ... to ubuntu:ubuntu

现在collectstatic在项目目录中以非 root 用户身份运行:

$ pwd
/home/ubuntu/django-gunicorn-nginx
$ python manage.py collectstatic
129 static files copied to '/var/www/supersecure.codes/static'.

最后,location/staticin添加一个变量/etc/nginx/sites-available/supersecure,您的 Nginx 站点配置文件:

server {
  location / {
    proxy_pass          http://localhost:8000;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-Proto $scheme;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }
}

请记住,您的域可能不是supersecure.codes,因此您需要自定义这些步骤以适用于您自己的项目。

您现在应该DEBUG在您的项目中关闭模式project/settings.py

# project/settings.py
DEBUG = False

自从您reload = Trueconfig/gunicorn/dev.py.

然后重启 Nginx:

$ sudo systemctl restart nginx

现在,再次刷新您的站点页面,并将鼠标悬停在页面文本上:

鼠标悬停时调用 JavaScript 放大的结果

这清楚地证明 JavaScript 函数enlarge()已经启动。要获得此结果,浏览器必须请求/static/js/greenlight.js. 这里的关键是浏览器直接从 Nginx 获取该文件,而不需要 Nginx 向 Django 询问它。

请注意上述过程的一些不同之处:您没有在任何地方添加新的 Django URL 路由或视图来交付 JavaScript 文件。这是因为,在运行之后collectstatic,Django 不再负责确定如何将 URL 映射到复杂视图并呈现该视图。Nginx 可以直接将文件交给浏览器。

事实上,如果您导航到您的域的等效项https://supersecure.codes/static/js/,您将看到/static由 Nginx 创建的传统文件系统树视图。这意味着更快、更有效地交付静态文件。

至此,您已经为使用 Django、Gunicorn 和 Nginx 的可扩展站点打下了良好的基础。另一个巨大的飞跃是为您的站点启用 HTTPS,接下来您将执行此操作。

使用 HTTPS 使您的网站做好生产准备

您可以通过更多步骤将您网站的安全性从良好提升到卓越,包括启用 HTTPS 和添加一组标头,以帮助 Web 浏览器以更安全的方式处理您的网站。启用HTTPS 会增加您网站的可信度,如果您的网站使用身份验证或与用户交换敏感数据,则这是必要的。

开启 HTTPS

要允许访问者通过 HTTPS 访问您的网站,您需要一个位于 Web 服务器上的SSL/TLS 证书。证书由证书颁发机构 (CA) 颁发。在本教程中,你将使用一个免费的CA称为让我们加密。要实际安装证书,您可以使用Certbot客户端,它会为您提供完全无痛的分步提示系列。

在开始使用 Certbot 之前,您可以预先告诉 Nginx 禁用 TLS 1.0 和 1.1 版,以支持 1.2 和 1.3 版。TLS 1.0 已停产 (EOL),而 TLS 1.1 包含多个已由 TLS 1.2 修复的漏洞。为此,请打开文件/etc/nginx/nginx.conf. 找到以下行:

# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

将其替换为更新的实现:

# File: /etc/nginx/nginx.conf
ssl_protocols TLSv1.2 TLSv1.3;

您可以使用nginx -t来确认您的 Nginx 支持 1.3 版:

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

现在您已准备好安装和使用 Certbot。在 Ubuntu Focal (20.04) 上,您可以使用snap来安装 Certbot:

$ sudo snap install --classic certbot
$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

请参阅 Certbot 的说明指南,了解不同操作系统和 Web 服务器的安装步骤。

在您可以使用 获取和安装 HTTPS 证书之前certbot,您需要对 VM 的安全组规则进行另一项更改。因为 Let's Encrypt 需要 Internet 连接来进行验证,所以您需要采取重要的步骤,将您的站点开放到公共 Internet。

修改您的入站安全规则以符合以下要求:

Reference Type Protocol Port Range Source
1 HTTP TCP 80 0.0.0.0/0
2 Custom All All security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

这里的关键变化是第一条规则,它允许来自所有来源的 HTTP 流量通过端口 80。您可以删除TCP:80列入白名单的 VM 公共 IP 地址的入站规则,因为这现在是多余的。其他两条规则保持不变。

然后,您可以再发出一个命令certbot来安装证书:

$ sudo certbot --nginx --rsa-key-size 4096 --no-redirect
Saving debug log to /var/log/letsencrypt/letsencrypt.log
...

这将创建一个 RSA 密钥大小为 4096 字节的证书。该--no-redirect选项告诉certbot不自动应用与自动 HTTP 到 HTTPS 重定向相关的配置。出于说明目的,您很快就会看到如何自己添加它。

您将完成一系列设置步骤,其中大部分步骤应该一目了然,例如输入您的电子邮件地址。当提示输入您的域名时,输入www以逗号分隔的域和子域:

www.supersecure.codes,supersecure.codes

完成这些步骤后,您应该会看到如下所示的成功消息:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/supersecure.codes/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/supersecure.codes/privkey.pem
This certificate expires on 2021-12-26.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this
  certificate in the background.

Deploying certificate
Successfully deployed certificate for supersecure.codes
  to /etc/nginx/sites-enabled/supersecure
Successfully deployed certificate for www.supersecure.codes
  to /etc/nginx/sites-enabled/supersecure
Congratulations! You have successfully enabled HTTPS
  on https://supersecure.codes and https://www.supersecure.codes

如果你cat在你的等价物上输出配置文件/etc/nginx/sites-available/supersecure,你会看到它certbot自动添加了一组与 SSL 相关的行:

# Nginx configuration: /etc/nginx/sites-available/supersecure
server {
  server_name               .supersecure.codes;
  listen                    80;
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

确保 Nginx 接受这些更改:

$ sudo systemctl reload nginx

要通过 HTTPS 访问您的站点,您需要添加一项最终安全规则。您需要允许流量通过TCP:443,其中 443 是 HTTPS 的默认端口。修改您的入站安全规则以符合以下要求:

Reference Type Protocol Port Range Source
1 HTTPS TCP 443 0.0.0.0/0
2 HTTP TCP 80 0.0.0.0/0
2 Custom All All security-group-id
3 SSH TCP 22 my-laptop-ip-address/32

这些规则中的每一个都有特定的目的:

  1. 规则 1允许来自所有来源的 HTTPS 流量通过端口 443。
  2. 规则 2允许来自所有来源的 HTTP 流量通过端口 80。
  3. 规则 3允许来自分配给同一安全组的网络接口和实例的入站流量,使用安全组 ID 作为源。这是您应该绑定到您的实例的默认 AWS 安全组中包含的规则。
  4. 规则 4允许您从您的个人计算机通过 SSH 访问您的 VM。

现在,在浏览器中重新导航到您的站点,但有一个关键区别。而不是http,指定https为协议:

https://www.supersecure.codes/myapp/

如果一切顺利,您应该会看到生活中最美丽的宝藏之一,那就是您的网站通过 HTTPS 传送:

通过 HTTPS 连接到您的 Django 应用程序

如果您使用 Firefox 并单击锁定图标,您可以查看有关保护连接所涉及的证书的更多详细信息:

您已安全地连接到此站点

您离安全网站又近了一步。此时,该站点仍可通过 HTTP 和 HTTPS 访问。这比以前好,但仍然不理想。

将 HTTP 重定向到 HTTPS

您的网站现在可以通过 HTTP 和 HTTPS 访问。使用 HTTPS 后,您几乎可以关闭 HTTP——或者至少在实践中接近它。您可以添加多项功能,以将尝试通过 HTTP 访问您的网站的任何访问者自动路由到 HTTPS 版本。编辑您的等价物/etc/nginx/sites-available/supersecure

# Nginx configuration: /etc/nginx/sites-available/supersecure
server {
  server_name               .supersecure.codes;
  listen                    80;
  return                    307 https://$host$request_uri;
}

server {
  location / {
    proxy_pass              http://localhost:8000;
    proxy_set_header        Host $host;
  }

  location /static {
    autoindex on;
    alias /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

添加的块告诉服务器将浏览器或客户端重定向到任何 HTTP URL 的 HTTPS 版本。您可以验证此配置是否有效:

$ sudo service nginx configtest /etc/nginx/sites-available/supersecure
 * Testing nginx configuration                                  [ OK ]

然后,告诉nginx重新加载配置:

$ sudo systemctl reload nginx

然后将GET带有--all标志的请求发送到您应用的 HTTP URL 以显示任何重定向链:

$ GET --all http://supersecure.codes/myapp/
HTTP/1.1 307 Temporary Redirect
Connection: keep-alive
Content-Length: 164
Content-Type: text/html
Date: Tue, 28 Sep 2021 02:16:30 GMT
Location: https://supersecure.codes/myapp/
Server: nginx

<html>
<head><title>307 Temporary Redirect</title></head>
<body bgcolor="white">
<center><h1>307 Temporary Redirect</h1></center>
<hr><center>nginx</center>
</body>
</html>

HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:16:30 GMT
Referrer-Policy: same-origin
Server: nginx
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

你可以看到这里实际上有两个响应:

  1. 初始请求接收307个状态码的响应重定向到HTTPS版本。
  2. 所述第二请求是到同一个URI,但与HTTPS方案,而不是HTTP制成。这一次,它通过200 OK响应接收它正在寻找的页面内容。

接下来,您将了解如何通过帮助浏览器记住该选择来超越重定向配置。

使用 HSTS 更进一步

单独使用时,此重定向设置存在一个小漏洞:

当用户手动输入 Web 域(提供不带 http:// 或 https:// 前缀的域名)或遵循普通的 http:// 链接时,对网站的第一个请求将使用普通 HTTP 以未加密方式发送。

大多数安全网站会立即发回重定向以将用户升级到 HTTPS 连接,但位置优越的攻击者可以发起中间人 (MITM) 攻击来拦截初始 HTTP 请求,并可以从然后。(来源

为了缓解这种情况,您可以添加一个HSTS 策略来告诉浏览器更喜欢 HTTPS,即使用户尝试使用 HTTP。这是仅使用重定向与在其旁边添加 HSTS 标头相比的细微差别:

  • 通过从 HTTP 到 HTTPS简单重定向,服务器通过说“再试一次,但使用 HTTPS”来回答浏览器。如果浏览器发出 1,000 个 HTTP 请求,它将被告知 1,000 次使用 HTTPS 重试。

  • 使用HSTS 标头,浏览器会在第一次请求后执行有效地HTTP替换为 HTTPS的前期工作。没有重定向。在第二种情况下,您可以将浏览器视为升级连接。当用户要求他们的浏览器访问您网站的 HTTP 版本时,他们的浏览器会简短地回应:“不,我要带您访问 HTTPS 版本。”

为了解决这个问题,您可以告诉 Django 设置Strict-Transport-Security标题。将这些行添加到您的项目中settings.py

# Add to project/settings.py
SECURE_HSTS_SECONDS = 30  # Unit is seconds; *USE A SMALL VALUE FOR TESTING!*
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

请注意,该SECURE_HSTS_SECONDS值在 30 秒处是短暂的。在这个例子中这是故意的。当您转向实际生产时,您应该增加此值。该安全头网站建议的2,592,000最小值,等于30天。

警告:在增加 的值之前SECURE_HSTS_SECONDS,请阅读 Django对 HTTP 严格传输安全性解释。在将 HSTS 时间窗口设置为大值之前,您应该首先确保 HTTPS 对您的站点有效。看到标头后,浏览器不会轻易让您改变该决定,而是坚持使用 HTTPS 而非 HTTP。

某些浏览器(例如 Chrome)可能允许您覆盖此行为并编辑 HSTS 策略列表,但您不应依赖该技巧。对于用户来说,这不会是一个非常流畅的体验。相反,保持一个较小的值,SECURE_HSTS_SECONDS直到您确信您的网站没有通过 HTTPS 提供任何回归服务。

当您准备好冒险时,您需要再添加一行 Nginx 配置。编辑您的等效项/etc/nginx/sites-available/supersecure以添加proxy_set_header指令:

  location / {
    proxy_pass          http://localhost:8000;
    proxy_set_header    Host $host;
    proxy_set_header    X-Forwarded-Proto $scheme;
  }

然后告诉 Nginx 重新加载更新的配置:

$ sudo systemctl reload nginx

添加的效果proxy_set_header是让 Nginx 向 Django 发送以下标头,这些标头包含在最初通过 HTTPS 在端口 443 上发送到 Web 服务器的中间请求中:

X-Forwarded-Proto: https

这直接挂钩到SECURE_PROXY_SSL_HEADER您在上面添加的值project/settings.py。这是必需的,因为 Nginx 实际上向 Gunicorn/Django 发送纯 HTTP 请求,因此 Django 无法知道原始请求是否为 HTTPS。由于location上面 Nginx 配置文件中的块是针对端口 443 (HTTPS),因此所有通过该端口发出的请求都应该让 Django 知道它们确实是 HTTPS。

Django 文档很好地解释了这一点:

但是,如果您的 Django 应用程序位于代理之后,则无论原始请求是否使用 HTTPS,代理都可能会“吞噬”。如果代理和 Django 之间存在非 HTTPS 连接,那么它is_secure()总是会返回False——即使是最终用户通过 HTTPS 发出的请求。相比之下,如果代理和 Django 之间存在 HTTPS 连接,那么它is_secure()总是会返回True——即使是最初通过 HTTP 发出的请求。(来源

您如何测试此标头是否有效?这是一种让您留在浏览器中的优雅方式:

  1. 在浏览器中,打开开发人员工具。导航到显示网络活动的选项卡。在 Firefox 中,这是Right Click → Inspect Element → Network

  2. 刷新页面。您应该首先将307 Temporary Redirect响应视为响应链的一部分。这是您的浏览器第一次看到Strict-Transport-Security标题。

  3. 将浏览器中的 URL 更改回 HTTP 版本,然后再次请求该页面。如果您使用的是 Chrome,您现在应该会看到一个307 Internal Redirect. 在 Firefox 中,您应该会看到200 OK响应,因为即使您试图告诉它使用 HTTP,您的浏览器也会自动直接转到 HTTPS 请求。虽然浏览器以不同方式显示它们,但这两种响应都表明浏览器已执行自动重定向。

如果您正在使用 Firefox,您应该会看到如下内容:

带有 HSTS 标头的立即 200 OK 响应

最后,您还可以使用来自控制台的请求来验证标头是否存在:

$ GET -ph https://supersecure.codes/myapp/
...
Strict-Transport-Security: max-age=30; includeSubDomains; preload

这证明您已Strict-Transport-Security使用 中的相应值有效地设置了标头project/settings.py。准备好后,您可以增加该max-age值,但请记住,这将不可逆转地告诉浏览器在这段时间内升级 HTTP。

设置Referrer-Policy标题

Django 3.x 还添加了控制Referrer-Policy标题的功能。您可以指定SECURE_REFERRER_POLICYproject/settings.py

# Add to project/settings.py
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"

这个设置如何工作?当您按照从页面A到页面B的链接时,您对页面B的请求在标题下包含页面A的 URL Referer。设置Referrer-Policy标头的服务器,您可以通过在 Django中设置标头,SECURE_REFERRER_POLICY控制将信息转发到目标站点的时间和数量。SECURE_REFERRER_POLICY可以采用许多公认的值,您可以在Mozilla 文档中详细了解这些值。

例如,如果您使用"strict-origin-when-cross-origin"并且用户的当前页面是https://example.com/page,则Referer标题会受到以下方式的约束:

Target Site Referer Header
https://example.com/otherpage https://example.com/page
https://mozilla.org https://example.com/
http://example.org (HTTP target) [None]

假设当前用户的页面是https://example.com/page

  • 如果用户点击链接到https://example.com/otherpageReferer将包含当前页面的完整路径。
  • 如果用户点击单独域的链接https://mozilla.orgReferer将排除当前页面的路径。
  • 如果用户http://example.org通过http://协议点击链接,Referer则为空白。

如果您将此行添加到project/settings.py您的应用主页并重新请求,您将看到一个新进入者:

$ GET -ph https://supersecure.codes/myapp/  # -ph: Show response headers only
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:31:36 GMT
Referrer-Policy: strict-origin-when-cross-origin
Server: nginx
Strict-Transport-Security: max-age=30; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

在本节中,您在保护用户隐私方面又迈出了一步。接下来,您将了解如何锁定站点对跨站点脚本 (XSS) 和数据注入攻击的漏洞。

添加Content-Security-Policy(CSP) 标头

您可以添加到站点的另一个重要 HTTP 响应标头是Content-Security-Policy(CSP)标头,它有助于防止跨站点脚本 (XSS)和数据注入攻击。Django 本身不支持此功能,但您可以安装django-cspMozilla 开发的一个小型中间件扩展:

$ python -m pip install django-csp

要使用默认值打开标题,请将这一行添加到project/settings.py现有MIDDLEWARE定义下:

# project/settings.py
MIDDLEWARE += ["csp.middleware.CSPMiddleware"]

你怎么能测试这个?好吧,您可以在其中一个 HTML 页面中包含一个链接,然后查看浏览器是否允许它与页面的其余部分一起加载。

编辑模板 atmyapp/templates/myapp/home.html以包含一个指向Normalize.css文件的链接,该文件是一个 CSS 文件,可帮助浏览器更一致地呈现所有元素并符合现代标准:

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
    <link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css"
    >
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

现在,在启用了开发人员工具的浏览器中请求页面。您将在控制台中看到类似以下的错误:

页面的设置阻止了资源的加载

哦哦。您错过了规范化的力量,因为您的浏览器无法加载normalize.css. 这就是它无法加载的原因:

  • project/settings.py包含CSPMiddleware在 Django 的MIDDLEWARE. 包括CSPMiddleware将标头设置为默认Content-Security-Policy值,即default-src 'self',其中'self'表示您站点自己的域。在本教程中,这是supersecure.codes.
  • 您的浏览器遵守此规则并禁止cdn.jsdelivr.net加载。CSP 是默认拒绝策略。

您必须选择加入并明确允许客户端的浏览器加载嵌入在您网站响应中的某些链接。要解决此问题,请将以下设置添加到project/settings.py

# project/settings.py
# Allow browsers to load normalize.css from cdn.jsdelivr.net
CSP_STYLE_SRC = ["'self'", "cdn.jsdelivr.net"]

接下来,再次尝试请求您网站的页面:

$ GET -ph https://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Security-Policy: default-src 'self'; style-src 'self' cdn.jsdelivr.net
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:37:19 GMT
Referrer-Policy: strict-origin-when-cross-origin
Server: nginx
Strict-Transport-Security: max-age=30; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

请注意,style-src指定'self' cdn.jsdelivr.netContent-Security-Policy标头值的一部分。这意味着浏览器应该只允许来自两个域的样式表

  1. supersecure.codes ('self')
  2. cdn.jsdelivr.net

style-src指令是可以成为Content-Security-Policy. 还有许多其他的,例如img-src,它指定了图像和网站图标的有效来源,以及script-src,它定义了JavaScript 的有效来源。

其中每一个都有相应的设置django-csp。例如,img-srcscript-src分别由CSP_IMG_SRC和设置CSP_SCRIPT_SRC。您可以查看django-csp文档以获取完整列表。

这是有关 CSP 标头的最后一个提示:尽早设置!当事情后来出现问题时,更容易查明原因,因为您可以更容易地隔离您添加的未加载的功能或链接,因为您没有最新的相应 CSP 指令。

生产部署的最后步骤

现在,您将完成在准备部署应用程序时可以采取的最后几个步骤。

首先,请确保您已经DEBUG = False在您的项目中进行了设置(settings.py如果您还没有这样做的话)。这可确保在出现 5xx 服务器端错误时不会泄露服务器端调试信息。

其次,SECURE_HSTS_SECONDS在您的项目中settings.py进行编辑,将Strict-Transport-Security标题的过期时间从 30 秒增加到建议的 30 天,相当于 2,592,000 秒:

# Add to project/settings.py
SECURE_HSTS_SECONDS = 2_592_000  # 30 days

接下来,使用生产配置文件重新启动 Gunicorn。将以下内容添加到config/gunicorn/prod.py

"""Gunicorn *production* config file"""

import multiprocessing

# Django WSGI application path in pattern MODULE_NAME:VARIABLE_NAME
wsgi_app = "project.wsgi:application"
# The number of worker processes for handling requests
workers = multiprocessing.cpu_count() * 2 + 1
# The socket to bind
bind = "0.0.0.0:8000"
# Write access and error info to /var/log
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
# Redirect stdout/stderr to log file
capture_output = True
# PID file so you can easily fetch process ID
pidfile = "/var/run/gunicorn/prod.pid"
# Daemonize the Gunicorn process (detach & enter background)
daemon = True

在这里,您进行了一些更改:

  • 您关闭了reload开发中使用的功能。
  • 您将工作人员的数量作为 VM 的 CPU 数量的函数,而不是对其进行硬编码。
  • 您允许loglevel默认为"info"而不是更详细的"debug".

现在你可以停止当前的 Gunicorn 进程并开始一个新的进程,用它的生产副本替换开发配置文件:

$ # Stop existing Gunicorn dev server if it is running
$ sudo killall gunicorn

$ # Restart Gunicorn with production config file
$ gunicorn -c config/gunicorn/prod.py

进行此更改后,您无需重新启动 Nginx,因为它只是将请求传递给相同的请求,address:host并且不应有任何可见的更改。但是,从长远来看,随着应用程序的扩展,以面向生产的设置运行 Gunicorn 会更健康。

最后,确保您已经完全构建了 Nginx 文件。这是完整的文件,包括您到目前为止添加的所有组件,以及一些额外的值:

# File: /etc/nginx/sites-available/supersecure
# This file inherits from the http directive of /etc/nginx/nginx.conf

# Disable emitting nginx version in the "Server" response header field
server_tokens             off;

# Use site-specific access and error logs
access_log                /var/log/nginx/supersecure.access.log;
error_log                 /var/log/nginx/supersecure.error.log;

# Return 444 status code & close connection if no Host header present
server {
  listen                  80 default_server;
  return                  444;
}

# Redirect HTTP to HTTPS
server {
  server_name             .supersecure.codes;
  listen                  80;
  return                  307 https://$host$request_uri;
}

server {

  # Pass on requests to Gunicorn listening at http://localhost:8000
  location / {
    proxy_pass            http://localhost:8000;
    proxy_set_header      Host $host;
    proxy_set_header      X-Forwarded-Proto $scheme;
    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_redirect        off;
  }

  # Serve static files directly
  location /static {
    autoindex             on;
    alias                 /var/www/supersecure.codes/static/;
  }

  listen 443 ssl;
  ssl_certificate /etc/letsencrypt/live/www.supersecure.codes/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/www.supersecure.codes/privkey.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}

作为复习,与您的 VM 相关的入站安全规则应该有一定的设置:

Type Protocol Port Range Source
HTTPS TCP 443 0.0.0.0/0
HTTP TCP 80 0.0.0.0/0
Custom All All security-group-id
SSH TCP 22 my-laptop-ip-address/32

综上所述,您的最终 AWS 安全规则集包含四个入站规则和一个出站规则:

Django 应用程序的最终安全规则集

将上述内容与您的初始安全规则集进行比较。请注意,您已经放弃了TCP:8000对 Django 应用程序开发版本的访问,并分别在端口 80 和 443 上通过 HTTP 和 HTTPS 打开了对 Internet 的访问。

您的网站现已准备好放映:

Nginx 和 Gunicorn 的最终配置

现在您已经将所有组件放在一起,您的应用程序可以通过 Nginx 通过 HTTPS 在端口 443 上访问。端口 80 上的 HTTP 请求被重定向到 HTTPS。Django 和 Gunicorn 组件本身并不暴露在公共 Internet 上,而是位于 Nginx 反向代理之后。

测试您网站的 HTTPS 安全性

您的网站现在比您开始本教程时安全得多,但不要相信我的话。有多种工具可以为您提供站点安全相关功能的客观评级,重点是响应标头和 HTTPS。

第一个是安全标头应用程序,它对从您的站点返回的HTTP 响应标头的质量进行评级。如果您一直在关注,您的网站应该准备好在那里获得 A 级或更好的评分。

第二个是SSL Labs,它将对您的 Web 服务器配置进行深入分析,因为它与SSL/TLS 相关。输入您站点的域,SSL Labs 将根据与 SSL/TLS 相关的各种因素的强度返回一个等级。如果你打电话certbot--rsa-key-size 4096和支持1.2和1.3关闭TLS 1.0和1.1,你应该建立很好地接收来自SSL实验室的A +评级。

作为检查,您还可以从命令行请求您站点的 HTTPS URL,以查看您在本教程中添加的更改的完整概述:

$ GET https://supersecure.codes/myapp/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Encoding: gzip
Content-Security-Policy: style-src 'self' cdn.jsdelivr.net; default-src 'self'
Content-Type: text/html; charset=utf-8
Date: Tue, 28 Sep 2021 02:37:19 GMT
Referrer-Policy: no-referrer-when-downgrade
Server: nginx
Strict-Transport-Security: max-age=2592000; includeSubDomains; preload
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8">
    <title>My secure app</title>
    <link rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/normalize.css@8.0.1/normalize.css"
    >
  </head>
  <body>
    <p><span id="changeme">Now this is some sweet HTML!</span></p>
    <script src="/static/js/greenlight.js"></script>
  </body>
</html>

这确实是一些甜蜜的 HTML。

结论

如果您已经按照本教程进行操作,那么您的站点与之前的独立开发 Django 应用程序相比,已经取得了很大的进步。您已经了解了 Django、Gunicorn 和 Nginx 如何结合在一起来帮助您安全地服务于您的站点。

在本教程中,您学习了如何:

  • 将您的 Django 应用程序从开发带到生产
  • 在现实世界的公共域上托管您的应用程序
  • GunicornNginx引入请求和响应链
  • 使用HTTP 标头来提高站点的 HTTPS 安全性

您现在有一组可重现的步骤来部署您的生产就绪 Django Web 应用程序。

您可以通过以下链接下载本教程中使用的 Django 项目:

进一步阅读

有了站点安全性,您永远无法 100% 到达那里,这是一个现实。您总是可以添加更多功能来进一步保护您的站点并生成更好的日志信息。

查看以下链接,了解您可以自行采取的其他步骤:

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。