Python 基础体系 · 第 79/112 篇。示例统一以 Python 3.14 为语言基线;第三方库使用与其兼容的现代稳定版本,版本敏感行为会单独说明。
Django 完整基础:项目、请求链、模型、模板、Admin 和安全
Django 是一个以“项目配置 + 可复用应用 + ORM + 模板 + 中间件 + 管理后台”为核心的全栈 Web 框架。它把 HTTP 请求处理、数据库访问、HTML 渲染、用户认证、表单校验和后台管理组织成一套相互连接的机制。
本文以 Django 5.2 文档为范围,示例使用 Python 3.14。Django 5.2 的官方教程说明其支持 Python 3.10 及更高版本,因此 Python 3.14 在该范围内。(docs.djangoproject.com)
一、先建立 Django 的整体模型
一个 Django 应用通常同时包含以下几层:
flowchart LR
C[浏览器或 API 客户端]
S[WSGI/ASGI 服务器]
D[Django 项目]
M[Middleware 中间件]
U[URL Dispatcher URL 调度器]
V[View 视图]
F[Form / Serializer]
O[ORM QuerySet]
DB[(数据库)]
T[Template 模板]
R[HttpResponse / DRF Response]
C --> S --> D --> M --> U --> V
V --> F
V --> O --> DB
V --> T --> R --> C
其中:
- 项目 Project:全局配置和 URL 根入口的集合。
- 应用 App:实现某一类业务功能的可复用模块。
- 请求链 Request Chain:请求从服务器进入 Django,经过中间件、URL 解析、视图、ORM、模板,再返回响应的过程。
- 模型 Model:Python 类与数据库表之间的映射。
- 模板 Template:把上下文数据渲染为 HTML 或其他文本。
- Admin:基于模型元数据生成的内部管理后台。
- 安全机制:对输入、输出、会话、请求来源、数据库查询和传输过程进行分层防护。
Django 并不要求所有项目都使用模板。一个项目可以只提供 JSON API,也可以只渲染 HTML,还可以同时提供网页和 API。
二、项目、应用与启动过程
2.1 创建虚拟环境
mkdir django-blog
cd django-blog
python3.14 -m venv .venv
source .venv/bin/activate
Windows PowerShell:
py -3.14 -m venv .venv
.venv\Scripts\Activate.ps1
安装 Django:
python -m pip install "Django>=5.2,<5.3"
查看版本:
python -m django --version
预期输出类似:
5.2.x
这里使用版本上限是为了避免未来安装到不兼容的更高主版本。生产项目应将实际依赖写入锁定文件,例如:
Django==5.2.6
具体补丁版本应根据项目当前依赖和安全公告确定,不应仅凭文章中的示例版本。
2.2 创建项目
django-admin startproject config .
目录大致如下:
django-blog/
├── manage.py
└── config/
├── __init__.py
├── asgi.py
├── settings.py
├── urls.py
└── wsgi.py
manage.py
manage.py 是项目级命令入口。它主要完成两件事:
- 设置
DJANGO_SETTINGS_MODULE; - 调用 Django 的命令执行器。
例如:
python manage.py check
python manage.py runserver
python manage.py makemigrations
python manage.py migrate
manage.py 本身不是 Web 服务器,也不是业务代码。它只是把项目配置加载进 Django 命令系统。
settings.py
settings.py 是配置模块,典型配置包括:
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
ASGI_APPLICATION = "config.asgi.application"
INSTALLED_APPS 决定 Django 会加载哪些应用。应用不仅可以包含 Python 代码,还可能包含模型、迁移、模板、静态文件和 Admin 注册代码。
wsgi.py 与 asgi.py
两者都暴露一个可被服务器调用的 Django 应用对象:
application = get_wsgi_application()
或:
application = get_asgi_application()
区别在于服务器接口:
- WSGI:传统同步 Web 服务器接口。
- ASGI:支持异步调用、WebSocket 等异步协议的接口。
async def 视图并不意味着所有数据库操作都会自动变成异步。同步 ORM 操作、第三方库和部署服务器是否真正支持异步,需要分别确认。不能因为使用 ASGI 就认为整个请求链天然非阻塞。
2.3 创建应用
python manage.py startapp blog
目录:
blog/
├── __init__.py
├── admin.py
├── apps.py
├── migrations/
│ └── __init__.py
├── models.py
├── tests.py
└── views.py
把应用加入项目:
# config/settings.py
INSTALLED_APPS = [
# Django 内置应用
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 自己的应用
"blog",
]
项目与应用的区别
项目回答:
这个 Django 系统如何配置、有哪些根 URL、使用哪些中间件和数据库?
应用回答:
这个系统中的某项业务功能如何实现?
例如,一个电商项目可以包含:
shop_project/
├── config/
├── accounts/
├── products/
├── orders/
└── payments/
项目不是业务领域,应用才通常对应业务领域或可复用功能。
三、请求链:一个请求到底如何执行
Django 的请求处理可以抽象为:
客户端
↓
WSGI/ASGI 服务器
↓
Django handler
↓
请求前中间件
↓
URL 解析
↓
视图
↓
模型、表单、模板或序列化器
↓
响应
↓
请求后中间件
↓
客户端
更精确地说,中间件形成一个嵌套调用结构:
response = middleware_1(
middleware_2(
middleware_3(
view(request)
)
)
)
请求阶段从上到下进入;响应阶段从下到上返回。
3.1 URL 调度器
项目根 URL:
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("blog.urls")),
]
应用 URL:
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.article_list, name="article-list"),
path("articles/<int:pk>/", views.article_detail, name="article-detail"),
]
URL 调度器的作用是:
- 从根 URL 配置开始匹配;
- 按
urlpatterns顺序尝试; - 匹配成功后提取路径参数;
- 调用对应视图;
- 匹配失败时返回 404。
path("articles/<int:pk>/", ...) 中的 <int:pk> 会把路径片段转换为整数,并作为关键字参数传给视图:
def article_detail(request, pk):
...
请求:
GET /articles/12/
相当于调用:
article_detail(request, pk=12)
URL 名称与反向解析
不要在模板和 Python 代码中到处硬编码 URL:
<a href="/articles/12/">查看</a>
应使用名称:
<a href="{% url 'blog:article-detail' article.pk %}">
查看
</a>
Python 中:
from django.urls import reverse
url = reverse("blog:article-detail", kwargs={"pk": article.pk})
这样修改路径结构时,不需要同步修改所有调用方。
3.2 视图
视图是接收请求并返回响应的可调用对象。
最简单的函数视图:
# blog/views.py
from django.http import HttpResponse
def hello(request):
return HttpResponse("Hello Django")
视图的输入通常是 HttpRequest,输出必须是响应对象,例如:
HttpResponseJsonResponseTemplateResponse- 重定向响应
错误示例:
def broken_view(request):
return {"message": "hello"} # 错误:不是 HttpResponse
Django 不能把普通字典直接当成 HTTP 响应返回。
3.3 render() 的真实含义
HTML 视图通常使用:
from django.shortcuts import render
def article_list(request):
articles = Article.objects.all()
return render(
request,
"blog/article_list.html",
{"articles": articles},
)
render() 并不是魔法函数,它可以理解为:
template = loader.get_template("blog/article_list.html")
content = template.render({"articles": articles}, request)
return HttpResponse(content)
它完成三个步骤:
- 根据模板名称加载模板;
- 构造上下文;
- 把上下文渲染为字符串并封装为 HTTP 响应。
3.4 中间件
中间件是包裹请求处理过程的组件。典型用途包括:
- 安全响应头;
- Session;
- CSRF;
- 用户认证;
- 请求日志;
- 统一异常处理;
- 访问控制;
- 性能计时。
例如:
class RequestLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
print("request:", request.method, request.path)
response = self.get_response(request)
print("response:", response.status_code)
return response
加入配置:
MIDDLEWARE = [
"config.middleware.RequestLogMiddleware",
"django.middleware.security.SecurityMiddleware",
# ...
]
中间件顺序有因果关系。例如:
AuthenticationMiddleware依赖SessionMiddleware;CsrfViewMiddleware应位于正常请求链中;- 自定义中间件如果需要读取
request.user,必须放在认证中间件之后; - 如果中间件在调用
get_response()之前直接返回响应,后续视图不会执行。
因此,中间件不是普通工具函数,而是请求链中的控制节点。
3.5 404、403、500 与异常传播
Django 中常见的异常与响应关系:
| 情况 | 常见结果 |
|---|---|
| URL 无法匹配 | 404 |
get_object_or_404() 找不到对象 |
404 |
| CSRF 校验失败 | 403 |
| 用户无权限 | 403 |
| 未捕获异常 | 500 |
| 显式重定向 | 301、302、303、307 或 308 |
示例:
from django.shortcuts import get_object_or_404, render
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(
request,
"blog/article_detail.html",
{"article": article},
)
它等价于:
try:
article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
raise Http404
生产环境不应依赖 runserver 承担部署职责。Django 官方文档将 runserver 定位为开发服务器,生产环境应使用合适的 WSGI 或 ASGI 服务器,并通过 check --deploy 检查部署配置。
四、模型:Python 类、数据库表和领域约束
Django 模型是继承自 django.db.models.Model 的 Python 类:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
Django 根据模型字段生成数据库结构。官方文档将模型层划分为模型定义、字段、关系、QuerySet、实例方法、迁移和事务等部分。(docs.djangoproject.com)
4.1 字段选项的区别
name = models.CharField(
max_length=100,
null=False,
blank=False,
unique=True,
)
几个选项作用不同:
max_length:字段最大长度,通常也影响表单校验。null:数据库是否允许存储NULL。blank:表单和模型验证是否允许空值。unique:数据库中是否允许重复。default:创建对象时的默认值。
null=True 与 blank=True 不是同一回事:
description = models.TextField(
null=True,
blank=True,
)
这表示:
- 数据库层可以存储
NULL; - 表单验证层允许留空。
对于字符串字段,项目通常需要明确是否使用 NULL 表示缺失,还是使用空字符串表示缺失。两种状态同时存在会增加查询复杂度:
description__isnull=True
description=""
4.2 关系字段
一对多:ForeignKey
class Comment(models.Model):
article = models.ForeignKey(
Article,
on_delete=models.CASCADE,
related_name="comments",
)
content = models.TextField()
数据库中通常是:
comment.article_id → article.id
on_delete=models.CASCADE 表示删除文章时级联删除评论。
常见选项:
CASCADE:级联删除;PROTECT:存在关联对象时禁止删除;SET_NULL:将外键设为NULL,要求null=True;SET_DEFAULT:设置为默认对象;DO_NOTHING:Django 不主动处理,可能由数据库约束报错。
选择 CASCADE 不是“默认最佳实践”。例如订单关联用户时,随意级联删除可能造成审计数据丢失;此时可能需要软删除或 PROTECT。
多对多:ManyToManyField
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
class Article(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, blank=True)
Django 会创建中间表:
article_id | tag_id
保存文章后才能添加标签,因为中间表需要双方的主键:
article = Article.objects.create(title="Django", body="...")
tag = Tag.objects.create(name="Python")
article.tags.add(tag)
一对一:OneToOneField
class Profile(models.Model):
user = models.OneToOneField(
"auth.User",
on_delete=models.CASCADE,
)
display_name = models.CharField(max_length=100)
它适合表示“一个对象最多对应另一个对象”的关系,例如用户和用户资料。
五、迁移:模型代码如何变成数据库结构
迁移是数据库模式的版本控制机制。模型修改后,通常执行:
python manage.py makemigrations
python manage.py migrate
5.1 两个命令的区别
makemigrations:
models.py 的变化
↓
生成 blog/migrations/0001_initial.py
它只生成迁移文件,不会修改数据库。
migrate:
迁移文件
↓
数据库表、列、索引、约束
它会根据迁移依赖顺序执行尚未应用的迁移。
查看迁移状态:
python manage.py showmigrations
可能输出:
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
auth
[X] 0001_initial
blog
[X] 0001_initial
[X] 表示已经应用,[ ] 表示尚未应用。
5.2 迁移不是简单的 SQL 文本
迁移描述的是数据库状态变化。例如:
class Migration(migrations.Migration):
dependencies = [
("blog", "0001_initial"),
]
operations = [
migrations.AddField(
model_name="article",
name="slug",
field=models.SlugField(default="", max_length=200),
),
]
增加非空字段时,如果已有数据,数据库无法凭空知道旧行应该填什么。因此 Django 要求:
- 提供默认值;
- 允许为空;
- 或分阶段迁移。
风险示例:
slug = models.SlugField(unique=True)
如果表中已有多行数据,直接添加唯一且非空字段通常无法安全完成。更稳妥的流程是:
- 先添加可空字段;
- 回填每一行;
- 检查唯一性;
- 再添加非空和唯一约束。
六、QuerySet:惰性查询与数据库执行时机
Django ORM 使用 QuerySet 表达查询:
articles = Article.objects.filter(
published_at__isnull=False
).order_by("-published_at")
此时通常还没有执行 SQL。QuerySet 具有惰性,以下操作会触发求值:
list(articles)
for article in articles:
print(article.title)
len(articles)
bool(articles)
articles[0]
例如:
queryset = Article.objects.filter(title__icontains="django")
这只是构造查询;直到模板循环或显式转换时,数据库才真正执行。
6.1 查询、更新和删除
Article.objects.get(pk=1)
Article.objects.filter(title__startswith="Django")
Article.objects.exclude(published_at__isnull=True)
Article.objects.order_by("-created_at")
Article.objects.values("id", "title")
更新:
Article.objects.filter(pk=1).update(title="新标题")
update() 直接执行 SQL,不会为每一行构造模型实例,也不会调用模型的 save() 方法。若依赖 save() 中的自定义逻辑,不能用 update() 替代。
删除:
Article.objects.filter(pk=1).delete()
对于批量删除,级联关系、数据库约束和信号行为都需要确认。
6.2 get()、filter() 与 first()
Article.objects.get(pk=1)
要求结果恰好一条:
- 没找到:
DoesNotExist; - 找到多条:
MultipleObjectsReturned。
Article.objects.filter(pk=1)
始终返回 QuerySet,即使没有结果也不会抛出 DoesNotExist。
Article.objects.filter(pk=1).first()
返回对象或 None。它适合“找第一条即可”的场景,但如果业务要求主键唯一,应使用 get(),这样数据异常能尽早暴露。
七、事务:多个数据库操作如何保持一致
事务保证一组数据库操作满足原子性:
from django.db import transaction
with transaction.atomic():
article = Article.objects.create(
title="事务示例",
body="正文",
)
Comment.objects.create(
article=article,
content="第一条评论",
)
如果代码块中抛出异常,事务回滚;如果正常结束,事务提交。
7.1 为什么需要事务
假设转账需要两步:
sender.balance -= amount
sender.save()
receiver.balance += amount
receiver.save()
如果第一步成功、第二步失败,系统会出现钱被扣除但未到账的中间状态。使用事务:
from django.db import transaction
with transaction.atomic():
sender.balance = F("balance") - amount
sender.save(update_fields=["balance"])
receiver.balance = F("balance") + amount
receiver.save(update_fields=["balance"])
这里还涉及并发问题。两个请求同时读取相同余额时,仅使用 Python 中的数值计算可能造成“丢失更新”。F() 表达式让数据库直接执行相对更新,减少读取—修改—写入之间的竞态。
对于需要读取后加锁的场景,可使用:
with transaction.atomic():
account = (
Account.objects
.select_for_update()
.get(pk=account_id)
)
if account.balance < amount:
raise ValueError("余额不足")
account.balance -= amount
account.save(update_fields=["balance"])
select_for_update() 通常要求数据库支持行级锁,并且必须在事务中使用。SQLite 的锁语义与 PostgreSQL、MySQL 不同,不能假设所有数据库表现一致。
八、模板:数据如何变成 HTML
Django 模板是文本模板,不是嵌入 Python 的执行环境。它支持变量、标签、过滤器、模板继承等机制,但默认不会执行任意 Python 表达式。(docs.djangoproject.com)
创建目录:
blog/
└── templates/
└── blog/
├── article_list.html
└── article_detail.html
列表视图:
# blog/views.py
from django.shortcuts import get_object_or_404, render
from .models import Article
def article_list(request):
articles = (
Article.objects
.filter(published_at__isnull=False)
.order_by("-published_at")
)
return render(
request,
"blog/article_list.html",
{"articles": articles},
)
def article_detail(request, pk):
article = get_object_or_404(Article, pk=pk)
return render(
request,
"blog/article_detail.html",
{"article": article},
)
模板:
<!-- blog/templates/blog/article_list.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>文章列表</title>
</head>
<body>
<h1>文章列表</h1>
{% for article in articles %}
<article>
<h2>
<a href="{% url 'blog:article-detail' article.pk %}">
{{ article.title }}
</a>
</h2>
<time>
{{ article.published_at|date:"Y-m-d H:i" }}
</time>
<p>{{ article.body|truncatewords:30 }}</p>
</article>
{% empty %}
<p>暂无文章。</p>
{% endfor %}
</body>
</html>
模板中:
{{ article.title }}
是变量输出。
{% for article in articles %}
是控制流程标签。
{{ article.body|truncatewords:30 }}
是过滤器链。
8.1 自动转义
Django 模板默认对 HTML 输出进行自动转义。例如上下文变量为:
{"name": "<script>alert(1)</script>"}
模板:
<p>{{ name }}</p>
输出中的 <、> 等字符会被转义,因此不会直接作为脚本执行。
危险写法:
{{ user_content|safe }}
safe 会告诉模板系统“不要再转义”。只有当内容经过可靠清洗,或本身是可信的固定 HTML 时,才可以使用。自动转义不能防护所有上下文,例如把用户输入直接拼进 JavaScript、CSS 或 URL 属性中仍可能产生漏洞。
8.2 模板继承
基础模板:
<!-- blog/templates/blog/base.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<title>{% block title %}博客{% endblock %}</title>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>
子模板:
<!-- blog/templates/blog/article_detail.html -->
{% extends "blog/base.html" %}
{% block title %}{{ article.title }}{% endblock %}
{% block content %}
<h1>{{ article.title }}</h1>
<p>{{ article.body }}</p>
{% endblock %}
模板继承解决的是重复 HTML 结构,不应把复杂业务逻辑塞进模板。查询、权限判断、数据聚合应在视图、表单、模型方法或服务层中完成。
九、表单与 POST 请求
Django 表单负责:
- 从请求中读取输入;
- 类型转换;
- 字段校验;
- 生成错误信息;
- 重新渲染表单;
- 必要时保存模型。
定义表单:
# blog/forms.py
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "body", "published_at"]
视图:
# blog/views.py
from django.shortcuts import redirect, render
from .forms import ArticleForm
def article_create(request):
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
form.save()
return redirect("blog:article-list")
else:
form = ArticleForm()
return render(
request,
"blog/article_form.html",
{"form": form},
)
模板:
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">保存</button>
</form>
这里使用 PRG,即 Post/Redirect/Get:
POST 保存
↓
302 Redirect
↓
GET 列表页
如果保存成功后直接返回 HTML,用户刷新页面可能重复提交 POST。重定向把刷新动作变成 GET,降低重复提交风险,但对于支付、订单等场景仍需使用幂等键和数据库约束。
十、Admin:基于模型元数据的管理后台
Django Admin 是面向可信内部用户的模型管理界面。官方文档明确指出,Admin 适合组织内部管理工具,不适合直接替代整个面向用户的前端。(docs.djangoproject.com)
创建管理员:
python manage.py createsuperuser
输入用户名、邮箱和密码后启动:
python manage.py runserver
访问:
/admin/
注册模型:
# blog/admin.py
from django.contrib import admin
from .models import Article, Comment, Tag
@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
list_display = ["title", "published_at", "created_at"]
list_filter = ["published_at", "created_at"]
search_fields = ["title", "body"]
ordering = ["-created_at"]
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ["article", "created_at"]
search_fields = ["content"]
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ["name"]
search_fields = ["name"]
ModelAdmin 是模型在 Admin 中的展示和操作配置。常用选项:
list_display:列表页显示列;list_filter:右侧过滤器;search_fields:搜索字段;ordering:默认排序;readonly_fields:只读字段;fieldsets:编辑页分组;autocomplete_fields:关联对象自动补全;list_select_related:列表页优化外键查询。
10.1 Admin 权限不是业务权限的全部
Django Admin 依赖认证、用户、组和模型权限。默认模型权限通常包括:
- add;
- change;
- delete;
- view。
但“能修改模型”不等于“能执行所有业务操作”。例如:
- 运营人员可以修改文章标题;
- 但不能修改已发布订单金额;
- 管理员可以查看用户;
- 但不应看到用户密码哈希以外的敏感认证信息。
复杂流程应通过自定义 Admin action、权限判断或独立业务视图实现,而不是仅依赖字段是否出现在 Admin 表单中。
十一、安全:Django 的防护边界
Django 的安全是分层的。开启某一个中间件,不代表整个应用自动安全。官方安全文档将 XSS、CSRF、SQL 注入、点击劫持、HTTPS、Host 校验、Session 和用户上传文件分别列为安全主题。(docs.djangoproject.com)
11.1 XSS:输出上下文中的不可信内容
XSS,即跨站脚本攻击,核心条件是:
攻击者可控输入
+
输入进入浏览器可执行上下文
+
没有正确编码或清洗
普通模板输出:
{{ comment.content }}
默认会自动转义,属于较安全路径。
危险写法:
<div>{{ comment.content|safe }}</div>
如果评论内容是:
<img src=x onerror=alert(1)>
使用 safe 后可能被浏览器解析为 HTML。
反例:
<script>
const name = "{{ user.name }}";
</script>
即使模板有 HTML 自动转义,也不能把它当作 JavaScript 字符串安全编码方案。应使用 JSON 编码、data-* 属性并配合正确的上下文编码。
11.2 CSRF:伪造浏览器携带凭证的请求
CSRF,即跨站请求伪造,常见于基于 Cookie 的会话认证。
攻击条件:
- 用户已经登录;
- 浏览器会自动携带 Session Cookie;
- 攻击者诱导用户访问恶意页面;
- 恶意页面向目标网站发起状态修改请求;
- 目标网站无法确认请求是否来自自己的页面。
Django 表单中应包含:
<form method="post">
{% csrf_token %}
...
</form>
前端发送 AJAX 请求时,需要读取 CSRF Cookie 并设置请求头:
fetch("/articles/create/", {
method: "POST",
headers: {
"X-CSRFToken": csrftoken,
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
CSRF 与 CORS 不是同一个问题:
- CORS 控制一个源的浏览器脚本能否读取另一个源的响应;
- CSRF 防止攻击者借助用户已有凭证执行状态修改。
如果 API 使用 Authorization: Bearer ...,通常不依赖浏览器自动携带的 Session Cookie,因此 CSRF 模型不同;但认证、权限、令牌泄露和重放风险仍然存在。
11.3 SQL 注入与 ORM 查询
安全写法:
Article.objects.filter(title__icontains=request.GET.get("q", ""))
Django 会把查询值作为参数绑定,而不是把输入拼接成 SQL 语句。
危险写法:
from django.db import connection
sql = f"SELECT * FROM blog_article WHERE title = '{title}'"
with connection.cursor() as cursor:
cursor.execute(sql)
如果 title 是:
' OR '1'='1
拼接后的 SQL 语义就可能被改变。
必须使用参数化:
with connection.cursor() as cursor:
cursor.execute(
"SELECT * FROM blog_article WHERE title = %s",
[title],
)
即使使用 ORM,RawSQL、extra()、动态排序字段和自定义数据库函数仍可能引入风险。用户只能选择白名单字段:
ORDERING_FIELDS = {
"newest": "-created_at",
"oldest": "created_at",
}
ordering = ORDERING_FIELDS.get(
request.GET.get("ordering"),
"-created_at",
)
articles = Article.objects.order_by(ordering)
不要直接写:
Article.objects.order_by(request.GET["ordering"])
11.4 Host、HTTPS 与安全 Cookie
生产配置至少应审查:
DEBUG = False
ALLOWED_HOSTS = [
"example.com",
"www.example.com",
]
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
但 SECURE_HSTS_SECONDS 不是可以盲目开启的配置。HSTS 会让浏览器在一段时间内强制使用 HTTPS;如果域名、子域名或证书链尚未准备好,可能造成访问中断。Django 的系统检查也会对 SECRET_KEY、CSRF 中间件、点击劫持和安全 Session Cookie 给出警告。(docs.djangoproject.com)
运行部署检查:
python manage.py check --deploy
它只能检查 Django 能识别的配置,不能代替:
- 反向代理配置检查;
- TLS 证书检查;
- 依赖漏洞扫描;
- 权限模型审计;
- 日志和密钥泄露审计。
11.5 SECRET_KEY 与敏感配置
不要把生产密钥提交到 Git:
SECRET_KEY = "django-insecure-..."
开发环境可以使用自动生成值,生产环境应从环境变量或密钥管理系统读取:
import os
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
如果密钥泄露,攻击者可能伪造签名数据、影响 Session 或其他依赖 Django 签名机制的功能。更换密钥可能使已有 Session、密码重置链接或签名数据失效,因此需要制定轮换与恢复流程。
11.6 用户上传文件
上传文件的风险不止是“文件名不可信”,还包括:
- 文件内容伪装;
- 超大文件导致资源耗尽;
- SVG 或 HTML 被浏览器当作可执行内容;
- 文件路径穿越;
- 压缩包炸弹;
- 恶意图片解析;
- 上传文件被同源直接执行。
表单示例:
class AvatarForm(forms.Form):
avatar = forms.ImageField()
视图:
def upload_avatar(request):
if request.method == "POST":
form = AvatarForm(request.POST, request.FILES)
if form.is_valid():
uploaded = form.cleaned_data["avatar"]
# 保存前还应检查大小、类型、权限和存储策略
不要仅依据客户端提供的 Content-Type 判断文件类型。生产部署通常应把用户上传内容放在独立域名或不可执行存储中,并限制大小和可接受格式。
十二、性能边界:N+1 查询
模型关系访问很方便,但也可能隐藏大量 SQL。
articles = Article.objects.all()
for article in articles:
print(article.author.username)
如果 author 是外键,可能产生:
1 次:查询文章
N 次:每篇文章查询作者
总计:1 + N 次查询
这就是 N+1 查询。
对于单值关系,例如 ForeignKey 和 OneToOneField:
articles = Article.objects.select_related("author")
它通常通过 SQL JOIN 一次取出作者。
对于多值关系,例如反向外键和多对多:
articles = Article.objects.prefetch_related("tags", "comments")
它通常执行有限次数的查询,再在 Python 层组装关系。
示例:
articles = (
Article.objects
.select_related("author")
.prefetch_related("tags")
.order_by("-created_at")
)
不要机械地给所有字段都加 select_related()。应根据实际查询路径、返回数据量和 SQL 执行计划选择。查询是否优化,最终要通过 Django Debug Toolbar、数据库日志或 QuerySet.explain() 验证,而不是凭代码外观判断。
十三、从 Django 页面到 REST API
Django 的传统视图一般返回 HTML:
HttpRequest → View → Template → HttpResponse
Django REST Framework,简称 DRF,则主要用于:
Request → Authentication → Permission → Throttle
→ Serializer → ViewSet
→ JSON Response
DRF 的 APIView 会使用自己的 Request 和 Response,并在进入处理方法前执行认证、权限、限流和内容协商。(django-rest-framework.org)
13.1 Serializer:验证输入和控制输出
定义序列化器:
# blog/serializers.py
from rest_framework import serializers
from .models import Article
class ArticleSerializer(serializers.ModelSerializer):
class Meta:
model = Article
fields = [
"id",
"title",
"body",
"published_at",
"created_at",
]
read_only_fields = ["id", "created_at"]
Serializer 有两个方向:
Python 对象 / QuerySet
↓
序列化
↓
JSON 等原生数据
请求数据
↓
反序列化
↓
字段校验
↓
validated_data
↓
create() / update()
例如:
serializer = ArticleSerializer(data=request.data)
if serializer.is_valid():
article = serializer.save()
错误时:
serializer.errors
可能得到:
{
"title": ["This field is required."]
}
ModelSerializer 是根据模型自动生成字段和部分校验逻辑的快捷方式,但它不等于完整业务校验。跨字段约束、当前用户归属、幂等性和状态机规则仍应显式实现。DRF 官方文档将 Serializer 描述为既负责模型对象到原生数据的转换,也负责输入反序列化和校验。(django-rest-framework.org)
13.2 ViewSet 与 Router
# blog/api.py
from rest_framework import permissions, viewsets
from .models import Article
from .serializers import ArticleSerializer
class ArticleViewSet(viewsets.ModelViewSet):
serializer_class = ArticleSerializer
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
def get_queryset(self):
return (
Article.objects
.filter(published_at__isnull=False)
.select_related("author")
.order_by("-published_at")
)
def perform_create(self, serializer):
serializer.save(author=self.request.user)
注册路由:
# config/urls.py
from django.contrib import admin
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from blog.api import ArticleViewSet
router = DefaultRouter()
router.register("articles", ArticleViewSet, basename="article")
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include(router.urls)),
]
ModelViewSet 通常提供:
| HTTP 方法 | 动作 |
|---|---|
GET /api/articles/ |
list |
POST /api/articles/ |
create |
GET /api/articles/1/ |
retrieve |
PUT /api/articles/1/ |
update |
PATCH /api/articles/1/ |
partial_update |
DELETE /api/articles/1/ |
destroy |
ViewSet 把一组相关动作组合在一个类中,Router 负责生成 URL。这样减少了重复路由配置,但也牺牲了一部分显式性;如果接口行为非常特殊,普通 APIView 或显式 Generic View 可能更清楚。(django-rest-framework.org)
13.3 权限:认证不等于授权
认证回答:
你是谁?
权限回答:
你是否可以做这件事?
DRF 权限类在视图主体执行前检查。如果检查失败,视图方法不会继续执行。默认权限策略是 AllowAny,因此生产 API 不应假设“配置了认证类就自动禁止匿名访问”。(django-rest-framework.org)
全局配置:
# config/settings.py
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated",
],
}
局部覆盖:
class ArticleViewSet(viewsets.ModelViewSet):
permission_classes = [
permissions.IsAuthenticatedOrReadOnly,
]
不同动作使用不同权限:
def get_permissions(self):
if self.action in {"list", "retrieve"}:
classes = [permissions.AllowAny]
else:
classes = [permissions.IsAuthenticated]
return [permission() for permission in classes]
仅设置权限类还不够。列表接口的对象可见范围通常应放到 get_queryset():
def get_queryset(self):
user = self.request.user
if user.is_staff:
return Article.objects.all()
return Article.objects.filter(author=user)
原因是对象级权限通常适用于 retrieve、update、destroy;列表接口需要通过 QuerySet 过滤掉不可见对象,否则可能把不应展示的数据先返回给客户端。(django-rest-framework.org)
创建对象时,权限规则常需要放在:
- Serializer 的
validate(); create();- ViewSet 的
perform_create()。
例如,不能只依赖客户端提交的:
{
"author": 999
}
而应由服务器根据当前认证用户设置作者:
def perform_create(self, serializer):
serializer.save(author=self.request.user)
13.4 分页
大型列表不能无限制返回全部数据。DRF 支持多种分页方式,但只有 Generic View 和 ViewSet 会自动执行分页;直接使用 APIView 时需要手动调用分页 API。(django-rest-framework.org)
全局配置:
REST_FRAMEWORK = {
"DEFAULT_PAGINATION_CLASS": (
"rest_framework.pagination.PageNumberPagination"
),
"PAGE_SIZE": 20,
}
响应可能类似:
{
"count": 105,
"next": "http://example.com/api/articles/?page=2",
"previous": null,
"results": [
{
"id": 1,
"title": "Django"
}
]
}
常见策略:
PageNumberPagination:?page=2,适合后台和普通列表;LimitOffsetPagination:?limit=20&offset=40,适合偏移式查询;CursorPagination:适合按稳定排序持续翻页,避免深偏移的部分问题。
分页不是单纯的 UI 功能。它会影响:
- 数据库排序;
- 深分页性能;
- 总数统计;
- 数据在分页期间新增或删除时的稳定性;
- 客户端缓存和重复消费。
13.5 限流
DRF 限流使用 Django 缓存后端保存计数状态。(django-rest-framework.org)
配置示例:
REST_FRAMEWORK = {
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"rest_framework.throttling.UserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "30/min",
"user": "300/min",
},
}
限流的基本状态可以表示为:
key = 用户 ID 或 IP + scope
count = 当前时间窗口内的请求数
若 count >= limit:
返回 429 Too Many Requests
否则:
count += 1
放行
DRF 内置限流实现存在并发竞态:高并发下可能放行少量超额请求。官方文档明确说明,如果业务必须严格保证并发条件下的请求数量,需要实现自己的限流类。(django-rest-framework.org)
因此:
- 一般 API 防滥用可以使用内置限流;
- 计费、配额、验证码、短信发送等严格计数场景,应使用原子 Redis 操作、数据库锁或专用网关;
- 限流不能替代认证、权限和输入校验。
十四、传统 Django、DRF 与 SQLAlchemy 的边界
Django ORM 和 SQLAlchemy 2.0 都能完成数据库访问,但抽象方式不同。
Django ORM
articles = (
Article.objects
.filter(published_at__isnull=False)
.select_related("author")
)
特点:
- 与 Django Model、迁移、Admin、表单深度集成;
- QuerySet 惰性求值;
- 事务通过
transaction.atomic()管理; - 适合 Django 全栈项目。
SQLAlchemy 2.0
概念上通常分为:
Engine → Connection / Session → ORM 映射 → select() → Transaction
示例:
from sqlalchemy import select
from sqlalchemy.orm import Session
with Session(engine) as session:
stmt = (
select(Article)
.where(Article.published_at.is_not(None))
.order_by(Article.published_at.desc())
)
articles = session.scalars(stmt).all()
两者都需要理解:
- 查询何时真正执行;
- Session 或事务的生命周期;
- 关系加载策略;
- 提交、回滚和异常恢复;
- N+1 查询;
- 数据库约束与并发。
不要把 Django 的 QuerySet、SQLAlchemy 的 Session 和 DRF 的 Serializer 当成同一层对象:
| 概念 | 所属层 |
|---|---|
| Django Model | 数据模型与 ORM 映射 |
| QuerySet | Django 查询表达式和结果集合 |
| SQLAlchemy Engine | 数据库连接池和方言入口 |
| SQLAlchemy Session | ORM 工作单元与身份映射 |
| DRF Serializer | API 输入输出与校验 |
| Django Template | 文本展示层 |
| Django Admin | 内部模型管理界面 |
例如,Serializer 不应该承担数据库连接池管理;Template 不应该承担业务权限判断;Admin 不应该被当成公开业务前端。
十五、一个完整的端到端请求
以请求:
GET /api/articles/12/
为例,完整路径如下:
- 浏览器或客户端建立 HTTP 请求;
- WSGI 或 ASGI 服务器把请求交给 Django;
- Django 创建
HttpRequest; - 中间件执行请求前逻辑;
- 根 URL 匹配
api/; - DRF Router 将路径映射到
ArticleViewSet.retrieve; - DRF 创建自己的
Request; - 执行认证;
- 执行权限检查;
- 调用
get_queryset(); - 根据主键获取文章;
ArticleSerializer把模型对象转换为原生 Python 数据;- 内容协商选择 JSON Renderer;
- 生成
Response; - 中间件执行响应后逻辑;
- 服务器把 HTTP 响应返回客户端。
如果第 8 步认证失败,可能得到 401 或 403;如果第 9 步权限失败,视图主体不会执行;如果第 11 步对象不存在,通常得到 404;如果序列化或数据库阶段出现未处理异常,可能得到 500。
这条链说明了一个重要事实:
“返回 JSON”只是响应格式变化,并没有消除 URL、认证、权限、数据库、事务、异常和安全问题。
十六、诊断方法:从现象反推请求链
16.1 页面返回 404
依次检查:
python manage.py check
然后确认:
- 根
config/urls.py是否include("blog.urls"); - 应用 URL 是否导入正确视图;
- 路径末尾斜杠是否一致;
<int:pk>是否收到非整数;- 视图内部是否调用了
get_object_or_404(); - 请求是否命中了错误的 URL 顺序。
16.2 页面返回 403
常见原因:
- POST 表单缺少
{% csrf_token %}; - AJAX 缺少
X-CSRFToken; - 用户已经认证但没有 DRF 权限;
ALLOWED_HOSTS配置错误;- 反向代理和 HTTPS 配置不一致。
不要看到 403 就直接关闭 CSRF。应先确认请求是否来自自己的页面、Cookie 是否正确、代理是否正确传递协议头。
16.3 页面查询次数异常
检查:
articles = (
Article.objects
.select_related("author")
.prefetch_related("tags")
)
然后观察:
- 模板是否访问了外键;
- Serializer 是否嵌套访问关系;
SerializerMethodField是否每次触发查询;- 列表接口是否调用了未优化的
get_queryset(); - 分页是否只限制了返回数量,却没有优化排序和关联加载。
16.4 迁移失败
先查看:
python manage.py showmigrations
python manage.py sqlmigrate blog 0001
sqlmigrate 可以查看某个迁移预计执行的 SQL。恢复时不要直接删除生产数据库迁移记录;应先判断:
- 迁移是否部分执行;
- 数据库是否已经改变;
- 迁移是否可逆;
- 是否需要新建修复迁移;
- 是否需要先备份和回滚应用版本。
十七、必须掌握的核心边界
Django 的自动化降低了常见 Web 开发的重复劳动,但以下边界不能被自动化掩盖:
-
模型定义不是完整数据治理
仍然需要数据库约束、索引、事务和并发设计。 -
ORM 不是性能保证
惰性 QuerySet、关系访问和嵌套 Serializer 都可能产生 N+1。 -
模板自动转义不是全上下文安全
HTML、JavaScript、CSS、URL 需要不同的编码策略。 -
认证不等于权限
登录用户仍然可能无权访问某个对象。 -
Admin 不等于业务系统
Admin 适合内部模型管理,不适合自动承载复杂公开流程。 -
限流不等于严格配额
内置限流在并发下可能存在少量超额,需要严格计数时应使用原子状态存储。 -
ASGI 不等于所有代码异步
同步 ORM 和阻塞式第三方库仍会阻塞执行线程。 -
迁移文件不等于数据迁移完成
模式变化、数据回填、约束启用和回滚策略需要分别验证。
掌握 Django 的关键,不是记住某个命令或类名,而是能够沿着完整请求链回答:
请求从哪里进入?
经过哪些中间件?
如何匹配到视图?
视图读取了哪些输入?
哪些代码访问数据库?
查询什么时候执行?
如何保证事务一致性?
谁可以访问对象?
数据如何序列化或渲染?
异常如何转成响应?
响应中是否泄露或执行了不可信内容?
当这些问题都能被明确回答时,项目、请求链、模型、模板、Admin 和安全才真正形成了一个可推理的 Django 基础体系。
系列导航与关联阅读
- 系列入口:Python 完整学习路线:从语言模型、并发到 Web、数据、AI 与生产交付
- 上一篇:Flask 完整基础:应用工厂、Context、Blueprint、扩展和部署
- 下一篇:Django REST Framework:Serializer、ViewSet、权限、分页和限流
- 延伸:SQLAlchemy 2.0:Engine、Session、映射、查询、事务和 N+1
官方资料
本文依据 Python 官方文档、相关 PEP 与生态项目官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论