02-进阶实战-测试-安全-部署

知识库
知识库文档
/tech-stacks/django/tutorial/02-进阶实战-测试-安全-部署.md

文档

Django 进阶实战 —— 测试、安全与部署

本章目标

  • 编写单元测试与集成测试
  • 理解 Django 安全机制与最佳实践
  • 使用 PostgreSQL + Gunicorn + Nginx 生产部署
  • 掌握 Django 性能优化技巧

1. 测试体系

1.1 单元测试

# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth.models import User
from blog.models import Post, Tag


class PostModelTest(TestCase):
    def setUp(self):
        """每个测试方法前运行"""
        self.user = User.objects.create_user(username="testuser", password="testpass")
        self.tag = Tag.objects.create(name="Django")
        self.post = Post.objects.create(
            title="Test Post",
            body="This is a test post content.",
        )
        self.post.tags.add(self.tag)

    def test_post_creation(self):
        """测试文章创建"""
        self.assertEqual(self.post.title, "Test Post")
        self.assertEqual(self.post.slug, "test-post")
        self.assertIsNotNone(self.post.created_at)

    def test_post_str(self):
        """测试 __str__ 方法"""
        self.assertEqual(str(self.post), "Test Post")

    def test_tag_relationship(self):
        """测试多对多关联"""
        self.assertEqual(self.post.tags.count(), 1)
        self.assertEqual(self.post.tags.first().name, "Django")
        self.assertIn(self.post, self.tag.posts.all())

1.2 视图测试

# blog/tests/test_views.py
from django.test import TestCase
from django.urls import reverse


class PostViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username="admin", password="admin123")
        self.post = Post.objects.create(
            title="Published Post", body="Content", status="published"
        )

    def test_post_list_view(self):
        """测试列表页返回 200 且包含文章"""
        url = reverse("post_list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Published Post")
        self.assertTemplateUsed(response, "blog/list.html")

    def test_post_detail_404(self):
        """测试不存在的文章返回 404"""
        url = reverse("post_detail", kwargs={"slug": "no-such-post"})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_create_post_requires_login(self):
        """测试未登录用户重定向到登录页"""
        url = reverse("post_create")
        response = self.client.get(url)
        self.assertRedirects(response, f"/accounts/login/?next={url}")

1.3 API 测试(DRF)

from rest_framework.test import APITestCase
from rest_framework import status


class PostAPITest(APITestCase):
    def test_list_posts(self):
        response = self.client.get("/api/posts/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_create_unauthenticated(self):
        response = self.client.post("/api/posts/", {"title": "x"}, format="json")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

运行测试

# 运行所有测试
python manage.py test

# 运行特定应用的测试
python manage.py test blog

# 运行特定测试类
python manage.py test blog.tests.test_models.PostModelTest

# 带覆盖率(需安装 coverage)
pip install coverage
coverage run manage.py test
coverage report
coverage html  # 生成 HTML 报告

2. 安全最佳实践

2.1 内置安全防护

Django 默认提供以下保护,无需额外配置:

防护项 机制
XSS 跨站脚本 模板自动 HTML 转义
CSRF 跨站请求伪造 CSRF 中间件 + {% csrf_token %}
SQL 注入 ORM 参数化查询
点击劫持 X-Frame-Options: DENY
内容嗅探 X-Content-Type-Options: nosniff

2.2 生产环境配置

# settings_production.py
import os

DEBUG = False  # 必须关闭!

# 秘密密钥从环境变量读取,不要硬编码
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# HTTPS 安全 Cookie
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

# HSTS(强制 HTTPS)
SECURE_HSTS_SECONDS = 31536000  # 一年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# 只允许指定 Host
ALLOWED_HOSTS = ["example.com", "www.example.com"]

# 密码哈希——使用最强算法
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]

# 文件上传大小限制
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024  # 10MB

2.3 常见安全问题

问题:开发中把 SECRET_KEY 提交到版本库

# .gitignore
.env
*.local
settings_local.py

问题:SQL 注入风险(避免 raw SQL)

# ❌ 危险!
Post.objects.raw(f"SELECT * FROM blog_post WHERE title = '{user_input}'")

# ✅ 安全——使用 ORM
Post.objects.filter(title=user_input)

3. 生产部署方案

架构

用户 → Nginx(反向代理 + 静态文件 + SSL)→ Gunicorn(WSGI 服务器)→ Django

3.1 PostgreSQL 配置

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "CONN_MAX_AGE": 600,  # 持久连接
        "OPTIONS": {
            "connect_timeout": 5,
        },
    }
}

3.2 Gunicorn 启动

gunicorn mysite.wsgi:application \
  -w 4 \                              # worker 进程数
  -b unix:/run/gunicorn.sock \        # Unix socket(比 TCP 更快)
  --access-logfile /var/log/gunicorn/access.log \
  --error-logfile /var/log/gunicorn/error.log \
  --log-level info

3.3 Nginx 配置

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 安全头
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    # 静态文件 —— Nginx 直接服务
    location /static/ {
        alias /var/www/mysite/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 媒体文件
    location /media/ {
        alias /var/www/mysite/media/;
    }

    # 动态请求代理到 Gunicorn
    location / {
        proxy_pass http://unix:/run/gunicorn.sock;
        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;
    }
}

4. 性能优化清单

4.1 数据库优化

# 使用 select_related(一对一/外键)和 prefetch_related(多对多)
# ❌ N+1 查询
posts = Post.objects.all()
# ✅ 一次 JOIN
posts = Post.objects.select_related("author").prefetch_related("tags")

# 只取需要的字段
Post.objects.values("id", "title", "created_at")

# 大数据量分批处理
Post.objects.filter(...).iterator(chunk_size=1000)

4.2 缓存策略

# 视图缓存
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 15分钟
def post_list(request):
    ...

# 模板片段缓存
{% load cache %}
{% cache 300 "sidebar" %}
    {# 昂贵的渲染逻辑 #}
{% endcache %}

# 低级缓存 API
from django.core.cache import cache
cache.set("hot_posts", posts, timeout=300)
posts = cache.get("hot_posts")

4.3 中间件精简

# 只保留必需的中间件,移除 CSRF(仅纯 API)、Message 等
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",  # 纯 API 可移除
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",              # 纯 API 可移除
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

思考题

  1. select_related()prefetch_related() 的区别是什么?
  2. Django 的 CSRF 保护原理是——攻击者如何构造 CSRF 攻击?Django 如何防护?
  3. 如何安全地存储 Django SECRET_KEY?有哪些反模式需避免?
  4. 纯 API 服务的 Django 可以移除哪些中间件和模板相关配置?

信息

路径
/tech-stacks/django/tutorial/02-进阶实战-测试-安全-部署.md
更新时间
2026/5/30