文档
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",
]
思考题
select_related()和prefetch_related()的区别是什么?- Django 的 CSRF 保护原理是——攻击者如何构造 CSRF 攻击?Django 如何防护?
- 如何安全地存储 Django SECRET_KEY?有哪些反模式需避免?
- 纯 API 服务的 Django 可以移除哪些中间件和模板相关配置?