29 KiB
29 KiB
Django / DRF Code Review Guide
Django / DRF 代码审查指南,覆盖安全审查、N+1 查询优化、Serializer 反模式、ViewSet 最佳实践、异步视图及生产安全配置等核心主题。
目录
安全审查
XSS 防护
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 防护
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 安全设置
# 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 注入防护
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}%"],
)
文件上传安全
# 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_related(ForeignKey / OneToOne)
# ❌ 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_related(M2M / 反向 ForeignKey)
# ❌ 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 缓存误用
# ❌ 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]
切片/索引不填充缓存
# ❌ 反复索引未评估的 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()
# ❌ 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()
# ❌ 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 反模式
排除敏感字段
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
缺少验证
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
嵌套写入
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 遗漏
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 最佳实践
选择正确的基类
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
用户级数据范围限定
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)
权限控制
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
分页和节流
# 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 在异步视图中的正确使用
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 ORM(Django 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
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})
异步视图中的事务
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})
同步中间件拖慢异步性能
# ❌ 同步中间件会把 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 迭代模式
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)
中间件与设置
生产安全配置清单
# 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
数据库连接安全
# 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 配置
# 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",
]
日志配置
# 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 = TrueSECURE_HSTS_SECONDS已设置(≥ 31536000)SECURE_CONTENT_TYPE_NOSNIFF = TrueX_FRAME_OPTIONS设置为DENY或SAMEORIGINALLOWED_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持久连接