Files
BOJ/.claude/skills/code-review-skill/reference/django.md
T

1031 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Django / DRF Code Review Guide
> Django / DRF 代码审查指南,覆盖安全审查、N+1 查询优化、Serializer 反模式、ViewSet 最佳实践、异步视图及生产安全配置等核心主题。
## 目录
- [安全审查](#安全审查)
- [N+1 查询优化](#n1-查询优化)
- [Serializer 反模式](#serializer-反模式)
- [ViewSet 最佳实践](#viewset-最佳实践)
- [异步视图](#异步视图)
- [中间件与设置](#中间件与设置)
- [Review Checklist](#review-checklist)
---
## 安全审查
### XSS 防护
```python
from django.utils.safestring import mark_safe
from django.template import engines
# ❌ mark_safe 绕过自动转义,直接渲染用户输入
def user_profile(request):
user_bio = request.user.bio # 用户可控
return HttpResponse(mark_safe(f"<p>{user_bio}</p>"))
# ❌ 在模板中手动关闭 autoescape
# {% autoescape off %}{{ user_bio }}{% endautoescape %}
# ✅ 让 Django 模板引擎自动转义
# template: <p>{{ user_bio }}</p>
# ✅ 必须使用 mark_safe 时,先手动转义
from django.utils.html import escape
def render_bio(bio: str) -> str:
return mark_safe(f"<p>{escape(bio)}</p>")
```
### CSRF 防护
```python
from django.views.decorators.csrf import csrf_exempt
# ❌ 禁用 CSRF 保护
@csrf_exempt
def process_payment(request):
# 任何恶意网站都可以提交表单
amount = request.POST["amount"]
charge(amount)
# ✅ 保留默认 CSRF 保护
from django.middleware.csrf import CsrfViewMiddleware
# settings.py — 确保 CSRF 中间件已启用
MIDDLEWARE = [
# ...
"django.middleware.csrf.CsrfViewMiddleware",
# ...
]
# ✅ API 使用 token 认证代替 CSRF
# settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
}
# ✅ 前端 AJAX 请求带上 CSRF token
# JavaScript: fetch("/api/endpoint/", {
# headers: {"X-CSRFToken": document.querySelector("[name=csrfmiddlewaretoken]").value}
# })
```
### Cookie 安全设置
```python
# settings.py
# ❌ 不安全的 cookie 配置
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = False
# ✅ 生产环境 cookie 安全配置
SESSION_COOKIE_SECURE = True # HTTPS only
SESSION_COOKIE_HTTPONLY = True # JavaScript 无法读取
SESSION_COOKIE_SAMESITE = "Lax" # 防止 CSRF
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Lax"
```
### SQL 注入防护
```python
from django.db import connection
# ❌ 字符串拼接 SQL — SQL 注入风险
def search_users(keyword):
query = f"SELECT * FROM auth_user WHERE username LIKE '%{keyword}%'"
with connection.cursor() as cursor:
cursor.execute(query)
# ❌ extra() 方法不安全
User.objects.extra(
where=[f"username = '{keyword}'"]
)
# ✅ 使用 ORM 参数化查询
def search_users(keyword):
return User.objects.filter(username__icontains=keyword)
# ✅ 原始 SQL 使用参数化
def search_users(keyword):
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM auth_user WHERE username LIKE %s",
[f"%{keyword}%"],
)
# ✅ 使用 raw() 参数化
User.objects.raw(
"SELECT * FROM auth_user WHERE username LIKE %s",
[f"%{keyword}%"],
)
```
### 文件上传安全
```python
# settings.py
# ❌ 默认上传配置不安全
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB — 可以接受
MEDIA_ROOT = "/var/www/uploads" # web 根目录下
ALLOWED_UPLOAD_TYPES = None # 没有类型限制
# ✅ 限制上传大小和位置
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10 MB
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # 2.5 MB in-memory
MEDIA_ROOT = "/srv/media/" # web 根目录之外
# ✅ 验证文件类型
import mimetypes
from pathlib import Path
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".pdf"}
def validate_upload(file):
ext = Path(file.name).suffix.lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValidationError(f"File type {ext} is not allowed.")
mime, _ = mimetypes.guess_type(file.name)
if mime not in {"image/jpeg", "image/png", "application/pdf"}:
raise ValidationError("Invalid MIME type.")
```
---
## N+1 查询优化
### select_relatedForeignKey / OneToOne
```python
# ❌ N+1: 每本书查一次出版社
books = Book.objects.all()
for book in books:
print(book.publisher.name) # 额外 N 条查询
# ✅ select_related 一次 JOIN 查询
books = Book.objects.select_related("publisher")
for book in books:
print(book.publisher.name) # 无额外查询
# ✅ 多层关系
books = Book.objects.select_related("publisher", "publisher__country")
# ✅ 只查需要的字段(延迟加载优化)
books = Book.objects.select_related("publisher").only(
"title", "publisher__name"
)
```
### prefetch_relatedM2M / 反向 ForeignKey
```python
# ❌ N+1: 每个作者查一次书
authors = Author.objects.all()
for author in authors:
print(author.books.all()) # 额外 N 条查询
# ✅ prefetch_related 两条查询 + Python 合并
authors = Author.objects.prefetch_related("books")
for author in authors:
print(list(author.books.all())) # 无额外查询
# ✅ 嵌套 prefetch
authors = Author.objects.prefetch_related(
"books",
"books__publisher",
)
# ✅ Prefetch 对象控制预查行为
from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch(
"books",
queryset=Book.objects.filter(published=True).only("title", "author_id"),
to_attr="published_books",
)
)
for author in authors:
print(author.published_books) # 已过滤,存在 to_attr 中
```
### QuerySet 缓存误用
```python
# ❌ count() 后再迭代 —— 两次查询
qs = Book.objects.all()
count = qs.count() # 查询 1: SELECT COUNT(*) — 不填充缓存
titles = [b.title for b in qs] # 查询 2: SELECT * — 重新评估
# ✅ 既要对象又要数量时,用 len() 触发一次评估并复用缓存
qs = Book.objects.all()
count = len(qs) # 查询 1: SELECT * — 全部加载并缓存
titles = [b.title for b in qs] # 复用缓存,无新查询
# ✅ 如果需要多次迭代,先转 list
books = list(Book.objects.all()) # 一次查询
count = len(books)
titles = [b.title for b in books]
```
### 切片/索引不填充缓存
```python
# ❌ 反复索引未评估的 QuerySet —— 每次都查库
qs = Book.objects.all()
qs[0] # 查询 1: SELECT ... LIMIT 1
qs[0] # 查询 2 — 切片/索引不会填充缓存
# ✅ 先整体评估,缓存保存所有行,之后索引走缓存
qs = Book.objects.all()
list(qs) # SELECT * — 评估并缓存全部行
qs[0] # 走缓存,无查询
qs[5] # 走缓存,无查询
# ✅ 只需要前 N 条时,切一次并转 list
books = list(Book.objects.all()[:10]) # 一次查询:SELECT ... LIMIT 10
first = books[0]
rest = books[1:] # 已是 Python list,无查询
```
### len() vs count()
```python
# ❌ len() 加载全部对象到内存
total = len(Book.objects.all()) # SELECT * FROM book — 全表加载
# ✅ count() 在数据库端计数
total = Book.objects.count() # SELECT COUNT(*) — 高效
# ✅ 如果已经需要 QuerySet 结果,再用 len
books = list(Book.objects.filter(published=True))
total = len(books) # 已在内存中,不需要额外查询
```
### if qs vs qs.exists()
```python
# ❌ if qs 加载全部记录
qs = Book.objects.filter(author_id=author_id)
if qs: # SELECT * FROM book WHERE ... — 全部加载
return qs[0]
# ✅ exists() 只检查是否有记录
if Book.objects.filter(author_id=author_id).exists():
return Book.objects.filter(author_id=author_id).first()
# ✅ 或者直接 get/first 判空
book = Book.objects.filter(author_id=author_id).first()
if book is not None:
return book
```
---
## Serializer 反模式
### 排除敏感字段
```python
from rest_framework import serializers
# ❌ __all__ 暴露所有字段,包括敏感数据
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__" # 密码 hash、is_superuser 等全部暴露
# ✅ 显式列出允许的字段
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "username", "email", "first_name", "last_name"]
# ✅ 使用 exclude 时也要注意
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = UserProfile
exclude = ["internal_notes", "admin_flags"]
# ✅ 密码字段用 write_only
class RegistrationSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True, min_length=8)
class Meta:
model = User
fields = ["id", "username", "email", "password"]
def create(self, validated_data):
user = User(**validated_data)
user.set_password(validated_data["password"])
user.save()
return user
```
### 缺少验证
```python
from rest_framework import serializers
# ❌ 没有验证,信任所有输入
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["quantity", "price", "discount"]
# ✅ 字段级验证
class OrderSerializer(serializers.ModelSerializer):
quantity = serializers.IntegerField(min_value=1, max_value=100)
price = serializers.DecimalField(max_digits=10, decimal_places=2, min_value=0)
discount = serializers.DecimalField(
max_digits=5, decimal_places=2, min_value=0, max_value=1, required=False
)
class Meta:
model = Order
fields = ["quantity", "price", "discount"]
# ✅ 对象级验证
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["quantity", "price", "discount"]
def validate(self, attrs):
if attrs.get("discount", 0) > 0.5 and attrs.get("quantity", 0) < 10:
raise serializers.ValidationError(
"Bulk discount requires minimum 10 items."
)
return attrs
# ✅ 自定义字段验证方法
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = ["start_date", "end_date", "room"]
def validate_start_date(self, value):
if value < date.today():
raise serializers.ValidationError("Start date cannot be in the past.")
return value
def validate(self, attrs):
if attrs["end_date"] <= attrs["start_date"]:
raise serializers.ValidationError("End date must be after start date.")
return attrs
```
### 嵌套写入
```python
from rest_framework import serializers
# ❌ 嵌套 Serializer 只读但没有实现 create/update
class TagSerializer(serializers.ModelSerializer):
class Meta:
model = Tag
fields = ["id", "name"]
class ArticleSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True) # 嵌套写入会失败
class Meta:
model = Article
fields = ["id", "title", "tags"]
# ✅ 方案 1: 嵌套只读 + PrimaryKeyRelatedField 写入
class ArticleSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True, read_only=True)
tag_ids = serializers.PrimaryKeyRelatedField(
queryset=Tag.objects.all(),
many=True,
write_only=True,
source="tags",
)
class Meta:
model = Article
fields = ["id", "title", "tags", "tag_ids"]
# ✅ 方案 2: 实现 create() 处理嵌套
class ArticleSerializer(serializers.ModelSerializer):
tags = TagSerializer(many=True)
class Meta:
model = Article
fields = ["id", "title", "tags"]
def create(self, validated_data):
tags_data = validated_data.pop("tags")
article = Article.objects.create(**validated_data)
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
article.tags.add(tag)
return article
def update(self, instance, validated_data):
tags_data = validated_data.pop("tags", None)
instance = super().update(instance, validated_data)
if tags_data is not None:
instance.tags.clear()
for tag_data in tags_data:
tag, _ = Tag.objects.get_or_create(**tag_data)
instance.tags.add(tag)
return instance
```
### read_only_fields 遗漏
```python
from rest_framework import serializers
# ❌ 计算字段和自动字段可被用户覆盖
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "body", "author", "created_at", "updated_at"]
# created_at, updated_at, author 可被客户端篡改
# ✅ 标记只读字段
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = ["id", "body", "author", "created_at", "updated_at"]
read_only_fields = ["author", "created_at", "updated_at"]
# ✅ 在视图中设置只读字段(如当前用户)
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
def get_serializer_context(self):
context = super().get_serializer_context()
context["request"] = self.request
return context
def perform_create(self, serializer):
serializer.save(author=self.request.user)
```
---
## ViewSet 最佳实践
### 选择正确的基类
```python
from rest_framework import viewsets
# ❌ ModelViewSet 提供完整 CRUD,但只需要读取
class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
# 暴露了 destroy, update, create — 标签不应被随意修改
# ✅ 只读场景用 ReadOnlyModelViewSet
class TagViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Tag.objects.all()
serializer_class = TagSerializer
# 只提供 list 和 retrieve
# ✅ 需要自定义操作时用 Mixin
from rest_framework import mixins
class TagViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView,
):
queryset = Tag.objects.all()
serializer_class = TagSerializer
```
### 用户级数据范围限定
```python
from rest_framework import viewsets
# ❌ 任何用户可以看到所有数据
class DocumentViewSet(viewsets.ModelViewSet):
queryset = Document.objects.all()
serializer_class = DocumentSerializer
# ✅ get_queryset 限定当前用户数据
class DocumentViewSet(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
def get_queryset(self):
return Document.objects.filter(
owner=self.request.user
).select_related("owner")
# ✅ 管理员看全部,普通用户看自己的
class DocumentViewSet(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
def get_queryset(self):
qs = Document.objects.select_related("owner")
if self.request.user.is_staff:
return qs
return qs.filter(owner=self.request.user)
# ✅ perform_create 自动关联当前用户
class DocumentViewSet(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
def get_queryset(self):
return Document.objects.filter(owner=self.request.user)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
```
### 权限控制
```python
from rest_framework import permissions, viewsets
# ❌ 没有权限控制
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# ✅ 类级别权限
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = [permissions.IsAuthenticated]
# ✅ 操作级别权限
from rest_framework.decorators import action
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
def get_permissions(self):
if self.action in ("list", "retrieve"):
return [permissions.AllowAny()]
if self.action == "create":
return [permissions.IsAuthenticated()]
return [permissions.IsAdminUser()]
# ✅ 自定义对象级权限
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
```
### 分页和节流
```python
# settings.py
# ❌ 没有分页和节流配置
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
],
}
# ✅ 全局分页和节流
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 20,
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "100/hour",
"user": "1000/hour",
},
}
# ✅ 自定义分页器
from rest_framework.pagination import PageNumberPagination
class StandardPagination(PageNumberPagination):
page_size = 25
page_size_query_param = "page_size"
max_page_size = 100
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
pagination_class = StandardPagination
```
---
## 异步视图
### 同步 ORM 在异步视图中的正确使用
```python
import asyncio
from asgiref.sync import sync_to_async
from django.http import JsonResponse
# ❌ 在 async 视图中直接调用同步 ORM — 阻塞事件循环
async def user_list(request):
users = User.objects.all() # Synchronous ORM call in async context!
data = [{"id": u.id, "name": u.username} for u in users]
return JsonResponse(data, safe=False)
# ✅ 使用 async ORMDjango 4.1+
async def user_list(request):
users = User.objects.all()
data = []
async for user in users: # async iteration
data.append({"id": user.id, "name": user.username})
return JsonResponse(data, safe=False)
# ✅ 使用 aget / afilter / acreate
async def user_detail(request, pk):
user = await User.objects.aget(pk=pk)
return JsonResponse({"id": user.id, "name": user.username})
# ✅ 复杂查询用 sync_to_async
@sync_to_async
def get_user_with_profile(pk):
return User.objects.select_related("profile").get(pk=pk)
async def user_profile(request, pk):
user = await get_user_with_profile(pk)
return JsonResponse({
"id": user.id,
"name": user.username,
"bio": user.profile.bio,
})
```
### 遗漏 await
```python
from django.http import JsonResponse
# ❌ 忘记 await — coroutine 不会执行,返回协程对象而非数据
async def user_detail(request, pk):
user = User.objects.aget(pk=pk) # Missing await!
# user 是一个 coroutine 对象,不是 User 实例
return JsonResponse({"name": user.username}) # RuntimeError
# ✅ 始终 await 异步 ORM 调用
async def user_detail(request, pk):
user = await User.objects.aget(pk=pk)
return JsonResponse({"name": user.username})
# ✅ 使用aget_or_404 的异步版本
from django.shortcuts import aget_object_or_404
async def user_detail(request, pk):
user = await aget_object_or_404(User, pk=pk)
return JsonResponse({"name": user.username})
```
### 异步视图中的事务
```python
from django.db import transaction
from asgiref.sync import sync_to_async
# ❌ transaction.atomic() 是同步的,不能直接在 async 中用
async def create_order(request):
async with transaction.atomic(): # Error! Not async-compatible
order = await Order.objects.acreate(total=100)
await OrderItem.objects.acreate(order=order, product_id=1)
return JsonResponse({"order_id": order.id})
# ✅ 用 sync_to_async 包装事务块
@sync_to_async
def _create_order_with_items():
with transaction.atomic():
order = Order.objects.create(total=100)
OrderItem.objects.create(order=order, product_id=1)
return order.id
async def create_order(request):
order_id = await _create_order_with_items()
return JsonResponse({"order_id": order_id})
# ✅ 多个操作打包到一个 sync_to_async 中
@sync_to_async
def _bulk_create_products(items):
with transaction.atomic():
products = Product.objects.bulk_create([Product(**i) for i in items])
return [p.id for p in products]
async def import_products(request):
ids = await _bulk_create_products(request.data)
return JsonResponse({"ids": ids})
```
### 同步中间件拖慢异步性能
```python
# ❌ 同步中间件会把 async 视图降级为同步执行
class TimingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request): # sync — blocks async views
start = time.time()
response = self.get_response(request)
elapsed = time.time() - start
response["X-Elapsed"] = str(elapsed)
return response
# ✅ async-capable 中间件:async def __call__,并在 __init__ 里标记实例
import time
from asgiref.sync import iscoroutinefunction, markcoroutinefunction
class TimingMiddleware:
async_capable = True
sync_capable = False
def __init__(self, get_response):
self.get_response = get_response
# get_response 是协程函数时标记自己,Django 才会 await 这个实例
if iscoroutinefunction(self.get_response):
markcoroutinefunction(self)
async def __call__(self, request):
start = time.time()
response = await self.get_response(request)
elapsed = time.time() - start
response["X-Elapsed"] = str(elapsed)
return response
# ✅ 要同时兼容同步和异步,用工厂函数 + 内置装饰器
from django.utils.decorators import sync_and_async_middleware
```
### async for 迭代模式
```python
from django.http import JsonResponse
# ❌ 同步迭代大型 QuerySet 在 async 视图中阻塞
async def export_users(request):
users = User.objects.all()
data = [] # 同步迭代阻塞事件循环
for user in users:
data.append({"id": user.id, "name": user.username})
return JsonResponse(data, safe=False)
# ✅ 使用 async for 异步迭代
async def export_users(request):
data = []
async for user in User.objects.all():
data.append({"id": user.id, "name": user.username})
return JsonResponse(data, safe=False)
# ✅ 大数据集使用 aiterator() + 分块处理
async def export_large_dataset(request):
data = []
async for user in User.objects.all().aiterator(chunk_size=500):
data.append({"id": user.id, "name": user.username})
return JsonResponse(data, safe=False)
# ✅ 使用 values() 减少内存
async def lightweight_export(request):
data = []
async for row in User.objects.values("id", "username"):
data.append(row)
return JsonResponse(data, safe=False)
```
---
## 中间件与设置
### 生产安全配置清单
```python
# settings.py — 生产环境必须的安全设置
# ❌ 开发默认值不应出现在生产环境
DEBUG = True
SECRET_KEY = "django-insecure-..."
ALLOWED_HOSTS = ["*"]
SECURE_SSL_REDIRECT = False
# ✅ 生产环境安全配置
# --- 基础安全 ---
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"] # 从环境变量读取
ALLOWED_HOSTS = ["example.com", "www.example.com"]
# --- HTTPS ---
SECURE_SSL_REDIRECT = True # HTTP 重定向到 HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# --- 安全头 ---
SECURE_HSTS_SECONDS = 31536000 # 1 year HSTS
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True # X-Content-Type-Options: nosniff
X_FRAME_OPTIONS = "DENY" # 防止 clickjacking
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
# --- 密码验证 ---
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"OPTIONS": {"min_length": 12}},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
]
# --- Session ---
SESSION_COOKIE_AGE = 3600 * 8 # 8 hours
SESSION_SAVE_EVERY_REQUEST = True
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
```
### 数据库连接安全
```python
# settings.py
# ❌ 明文密码在代码中
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "mydb",
"USER": "admin",
"PASSWORD": "hunter2", # 不要硬编码密码
"HOST": "localhost",
"PORT": "5432",
}
}
# ✅ 从环境变量读取
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": os.environ.get("DB_NAME", "mydb"),
"USER": os.environ.get("DB_USER", "mydb_user"),
"PASSWORD": os.environ["DB_PASSWORD"],
"HOST": os.environ.get("DB_HOST", "localhost"),
"PORT": os.environ.get("DB_PORT", "5432"),
"OPTIONS": {
"sslmode": "require", # 强制 SSL 连接
},
"CONN_MAX_AGE": 60, # 持久连接
}
}
```
### CORS 配置
```python
# settings.py (using django-cors-headers)
# ❌ 允许所有来源
CORS_ALLOW_ALL_ORIGINS = True
# ✅ 限制允许的来源
CORS_ALLOWED_ORIGINS = [
"https://example.com",
"https://app.example.com",
]
# ✅ 生产环境 CORS 设置
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = os.environ.get("CORS_ORIGINS", "").split(",")
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
CORS_ALLOW_HEADERS = [
"authorization",
"content-type",
"x-csrftoken",
]
```
### 日志配置
```python
# settings.py
# ❌ 默认日志配置(或不配置)
LOGGING = {}
# ✅ 生产环境日志配置
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
},
},
"handlers": {
"file": {
"level": "INFO",
"class": "logging.handlers.RotatingFileHandler",
"filename": "/var/log/django/app.log",
"maxBytes": 10 * 1024 * 1024, # 10 MB
"backupCount": 5,
"formatter": "verbose",
},
},
"loggers": {
"django": {
"handlers": ["file"],
"level": "INFO",
"propagate": False,
},
"myapp": {
"handlers": ["file"],
"level": "DEBUG" if DEBUG else "INFO",
"propagate": False,
},
},
}
```
---
## Review Checklist
### 安全审查
- [ ] 没有使用 `mark_safe` 渲染未转义的用户输入
- [ ] CSRF 中间件已启用,没有 `@csrf_exempt`
- [ ] Session 和 CSRF cookie 设置 `Secure`, `HttpOnly`, `SameSite`
- [ ] SQL 查询使用参数化(ORM 或参数化 `raw()`),无字符串拼接
- [ ] 文件上传有类型和大小限制
- [ ] `SECRET_KEY` 从环境变量读取,不在代码仓库中
- [ ] `DEBUG = False` 在生产环境
### HTTPS 与安全头
- [ ] `SECURE_SSL_REDIRECT = True`
- [ ] `SECURE_HSTS_SECONDS` 已设置(≥ 31536000
- [ ] `SECURE_CONTENT_TYPE_NOSNIFF = True`
- [ ] `X_FRAME_OPTIONS` 设置为 `DENY``SAMEORIGIN`
- [ ] `ALLOWED_HOSTS` 不包含 `"*"`
- [ ] 数据库连接使用 SSL
### N+1 查询
- [ ] ForeignKey 关系使用 `select_related`
- [ ] M2M / 反向关系使用 `prefetch_related`
- [ ] 没有在循环中访问关联对象
- [ ] 使用 `count()` 代替 `len(queryset)` 做计数
- [ ] 使用 `exists()` 代替 `if queryset` 做存在性检查
- [ ] 大数据集使用 `only()` / `defer()``values()` 减少查询字段
- [ ] 切片后的 QuerySet 不重复迭代
### Serializer
- [ ] 不使用 `fields = "__all__"` 在敏感模型上
- [ ] 密码字段标记 `write_only=True`
- [ ] 有字段级和对象级验证
- [ ] 嵌套写入实现了 `create()` / `update()` 或使用 `read_only=True`
- [ ] 计算字段和自动字段在 `read_only_fields`
- [ ] Serializer 不包含不应被修改的字段
### ViewSet
- [ ] 只读场景使用 `ReadOnlyModelViewSet`
- [ ] `get_queryset()` 限定当前用户数据范围
- [ ] 设置了 `permission_classes`
- [ ] 创建时用 `perform_create()` 自动设置 owner/author
- [ ] 配置了分页(全局或 ViewSet 级别)
- [ ] 配置了节流(throttling
### 异步视图
- [ ] async 视图中不直接调用同步 ORM(用 `aget`/`afilter`/`sync_to_async`
- [ ] 所有异步调用都有 `await`
- [ ] `transaction.atomic()``sync_to_async` 包装
- [ ] 中间件标记 `async_capable = True` 以避免降级
- [ ] 大型 QuerySet 使用 `async for` + `aiterator()`
### 生产配置
- [ ] `CORS_ALLOWED_ORIGINS` 不使用 `CORS_ALLOW_ALL_ORIGINS = True`
- [ ] 密码验证器已配置(最小长度、常见密码检查)
- [ ] Session 过期时间合理(`SESSION_COOKIE_AGE`
- [ ] 日志配置使用 RotatingFileHandler,不在生产环境输出到 stdout
- [ ] 数据库连接使用 `CONN_MAX_AGE` 持久连接