CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Django 实战入门/课时/表单与安全
RawBlame
课时目录12 节

Django 实战入门

  1. 01Django 概览
  2. 02项目结构
  3. 03URL 路由
  4. 04视图层
  5. 05模型与 ORM
  6. 06查询与优化
  7. 07模板 DTL
  8. 08Admin 与认证
  9. 09表单与安全
  10. 10DRF 与异步任务
  11. 11测试与部署
  12. 12综合练习

表单与安全

ModelForm、CSRF 与消息。

30 分钟

表单与安全

这一课你将能做到

  1. 用 Form 和 ModelForm 构建带完整校验的表单,渲染成 HTML 并处理提交
  2. 掌握 CSRF 防护的原理和实操,理解 SameSite Cookie、CSP、XSS 的关系
  3. 用 messages 框架给用户操作反馈,构建完整的「提交-校验-反馈」闭环

为什么要学

表单是 Web 应用最危险的入口——所有用户输入都可能携带恶意代码、注入攻击或非法数据。Django 的表单系统把「接收输入 → 校验 → 清洗 → 反馈」做成了一条流水线,配合 CSRF 防护和 XSS 过滤,帮你挡住 90% 的常见攻击。

和 Spring Boot 对比:Spring 用 @Valid + @RequestBody 做 Bean Validation,安全靠 Spring Security 配置。Django 的表单系统更「主动」:自动渲染 HTML、自动校验、自动回填错误。两种思路:Java 是「数据驱动,前端接管 UI」;Django 是「服务端主导,UI 由后端生成」。

先建立图像

用户提交表单
   │
   ▼
POST 数据 → Form(data=request.POST)
   │
   ├─ is_valid() = False
   │     └─ 回填错误信息 → 重新渲染表单(带错误提示)
   │
   └─ is_valid() = True
         ├─ cleaned_data → 干净、类型转换后的数据
         └─ 保存 / 业务处理 → redirect → messages 提示成功

CSRF 防护原理:

服务端生成 token → 塞进 session + 表单隐藏字段
                        │
用户提交表单 ←──────────┘
   │
服务端比对 session 中的 token 和表单提交的 token
   ├─ 一致:放行
   └─ 不一致:403 Forbidden

跟着做

步骤 1:基础 Form 类

blog/forms.py:

from django import forms
from django.core.validators import MinLengthValidator


class ContactForm(forms.Form):
    name = forms.CharField(
        max_length=50,
        label="姓名",
        widget=forms.TextInput(attrs={"class": "form-control", "placeholder": "请输入姓名"})
    )
    email = forms.EmailField(
        label="邮箱",
        widget=forms.EmailInput(attrs={"class": "form-control"})
    )
    subject = forms.ChoiceField(
        choices=[("feedback", "反馈"), ("bug", "Bug 报告"), ("other", "其他")],
        label="类型"
    )
    message = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 5, "class": "form-control"}),
        validators=[MinLengthValidator(10, "内容至少 10 个字符")],
        label="内容"
    )
    cc_myself = forms.BooleanField(required=False, label="抄送给自己")

    # 自定义校验:以 clean_字段名 命名
    def clean_message(self):
        message = self.cleaned_data["message"]
        if "傻逼" in message:
            raise forms.ValidationError("内容包含敏感词")
        return message

视图:

from django.shortcuts import render, redirect
from django.contrib import messages
from .forms import ContactForm


def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            # cleaned_data 是类型转换后的干净数据
            name = form.cleaned_data["name"]
            email = form.cleaned_data["email"]
            # ... 发送邮件等逻辑 ...
            messages.success(request, "提交成功!我们会尽快回复。")
            return redirect("blog:contact")
        # is_valid() 为 False 时,form 自动携带错误信息
    else:
        form = ContactForm()

    return render(request, "blog/contact.html", {"form": form})

模板:

<form method="post" novalidate>
    {% csrf_token %}

    {% if form.non_field_errors %}
        <div class="alert alert-danger">{{ form.non_field_errors }}</div>
    {% endif %}

    {% for field in form %}
        <div class="form-group {% if field.errors %}has-error{% endif %}">
            <label for="{{ field.id_for_label }}">{{ field.label }}</label>
            {{ field }}
            {% for error in field.errors %}
                <span class="error">{{ error }}</span>
            {% endfor %}
            {% if field.help_text %}
                <small>{{ field.help_text }}</small>
            {% endif %}
        </div>
    {% endfor %}

    <button type="submit" class="btn btn-primary">提交</button>
</form>

步骤 2:ModelForm——表单直连模型

from django.forms import ModelForm
from .models import Article


class ArticleForm(ModelForm):
    class Meta:
        model = Article
        fields = ["title", "slug", "category", "tags", "content", "status"]
        # exclude = ["author", "views"]  # 或者用 exclude 排除字段
        widgets = {
            "content": forms.Textarea(attrs={"rows": 20}),
            "tags": forms.CheckboxSelectMultiple(),
        }
        labels = {
            "title": "文章标题",
            "content": "正文",
        }
        help_texts = {
            "slug": "URL 友好标识,只能包含字母、数字、连字符",
        }

    def clean_slug(self):
        slug = self.cleaned_data["slug"]
        # 编辑时排除自己
        qs = Article.objects.filter(slug=slug)
        if self.instance.pk:
            qs = qs.exclude(pk=self.instance.pk)
        if qs.exists():
            raise forms.ValidationError("该 slug 已被使用")
        return slug

视图(Create/Update 一把梭):

from django.views.generic import CreateView, UpdateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy


class ArticleCreateView(LoginRequiredMixin, CreateView):
    model = Article
    form_class = ArticleForm
    template_name = "blog/article_form.html"
    success_url = reverse_lazy("blog:index")

    def form_valid(self, form):
        form.instance.author = self.request.user  # 自动填充作者
        return super().form_valid(form)


class ArticleUpdateView(LoginRequiredMixin, UpdateView):
    model = Article
    form_class = ArticleForm
    template_name = "blog/article_form.html"
    success_url = reverse_lazy("blog:index")

    def get_queryset(self):
        # 只允许编辑自己的文章
        return super().get_queryset().filter(author=self.request.user)

步骤 3:CSRF 防护深度解析

什么攻击:恶意网站诱导用户浏览器向目标站发请求,浏览器自动带 Cookie,服务端误以为是用户本人操作。

Django 的防护:

<!-- 模板中必须带 token -->
<form method="post">
    {% csrf_token %}
    ...
</form>

渲染后:

<input type="hidden" name="csrfmiddlewaretoken" value="abc123...">

Ajax 请求:

// 从 cookie 读取
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

fetch("/api/articles/", {
    method: "POST",
    headers: {
        "X-CSRFToken": getCookie("csrftoken"),
        "Content-Type": "application/json",
    },
    body: JSON.stringify({title: "test"})
});

豁免 CSRF(API 场景):

from django.views.decorators.csrf import csrf_exempt

@csrf_exempt  # 仅用于无状态 API,生产环境用 token 认证代替
def api_endpoint(request):
    ...

步骤 4:Messages 框架

# settings.py(默认已配置)
MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"

# 视图里
from django.contrib import messages

messages.success(request, "文章发布成功!")
messages.info(request, "这是一条提示")
messages.warning(request, "请注意格式")
messages.error(request, "操作失败")

模板渲染:

{% if messages %}
    <div class="messages">
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }}">
                {{ message }}
            </div>
        {% endfor %}
    </div>
{% endif %}

message.tags 是级别字符串(success/info/warning/error),可以直接当 CSS class 用。

步骤 5:其他安全要点

# settings.py 安全清单
SECURE_BROWSER_XSS_FILTER = True          # X-XSS-Protection 头
SECURE_CONTENT_TYPE_NOSNIFF = True        # X-Content-Type-Options: nosniff
X_FRAME_OPTIONS = "DENY"                  # 防点击劫持
CSRF_COOKIE_SECURE = True                 # 仅 HTTPS 传输 CSRF cookie
SESSION_COOKIE_SECURE = True              # 仅 HTTPS 传输 session cookie
SECURE_SSL_REDIRECT = True                # 强制跳转 HTTPS
SECURE_HSTS_SECONDS = 31536000            # HSTS 一年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

讲清楚要点

  1. Form vs ModelForm:Form 是通用表单,字段手动定义。ModelForm 直接从 Model 生成字段,save() 一键入库。90% 的 CRUD 场景用 ModelForm。
  2. is_valid() 的副作用:调用后会填充 cleaned_data(干净数据)和 errors(错误信息)。必须先调用 is_valid() 才能访问 cleaned_data。
  3. clean_<fieldname>() 是字段级校验钩子:在该字段的内置校验通过后执行。clean() 是表单级校验,可以跨字段比较(如「确认密码 == 密码」)。
  4. CSRF 的本质是「双重提交验证」:token 既存在 cookie 又存在表单字段,服务端比对两者。攻击者无法读到目标站的 cookie(同源策略),所以伪造不了。
  5. Messages 是一次性的:消息存 session,下一次请求读出后自动清除。适合「操作后跳转 + 提示」的场景,不适合常驻通知。
  6. form.save(commit=False):返回未保存的实例,可以先修改字段(如填充 author)再 save()。CreateView 内部就是这么做的。

你可能会懵的地方

  • request.POST 和 request.FILES 为什么要分开传? 表单有文件上传时,HTML 必须用 enctype="multipart/form-data",Django 把普通字段放 request.POST,文件放 request.FILES。ModelForm 用法:ArticleForm(request.POST, request.FILES)。

  • 校验错误信息能自定义吗? 能。字段定义时传 error_messages:forms.CharField(error_messages={"required": "必填", "max_length": "太长了"})。

  • CSRF token 会不会过期? 会。默认 token 和 session 绑定,session 过期 token 失效。Ajax 长时间停留的页面,建议每次请求前刷新 token。

  • {% csrf_token %} 和 cookie 里的 csrftoken 是一个吗? 值相同但用途不同。表单里的隐藏字段是 POST body 提交,cookie 是浏览器自动带。Django 比对两者,防止攻击者只伪造 cookie。

  • 富文本编辑器怎么防 XSS? 不要用 |safe 直接输出用户 HTML。用 bleach 库白名单过滤:bleach.clean(user_html, tags=["p", "strong", "em"], strip=True)。

错法对照

错法问题正解
form = ArticleForm(request.POST) 后直接 form.save()没校验,脏数据入库先 if form.is_valid():
文件上传忘记 request.FILES文件丢失ArticleForm(request.POST, request.FILES)
API 视图全局 @csrf_exempt任意站点可调用用 Token 认证(DRF 的 TokenAuthentication)
用户输入直接 `safe` 输出XSS 漏洞
cleaned_data 当 dict 用 cleaned_data["field"]KeyErrorcleaned_data.get("field") 或确认 is_valid() 通过
ModelForm 暴露所有字段用户能改 author、views用 fields 白名单,不要 exclude

卡点急救

  1. 表单提交后页面空白 / 永远走不到 is_valid:检查模板是否有 {% csrf_token %},表单 method 是否是 post。
  2. 文件上传后找不到文件:检查 MEDIA_ROOT 配置,settings.py 里的 MEDIA_URL,以及 urls.py 是否 serve 了 media(开发环境)。
  3. This field is required 即使填了值:HTML 表单的 name 属性必须和 Form 字段名一致。检查浏览器开发者工具的 Network 里实际提交了什么。

面试常问

  • CSRF 攻击的原理是什么?Django 如何防护? 攻击者诱导用户浏览器向目标站发请求,浏览器自动携带 Cookie,服务端无法区分是用户主动操作还是被诱导。Django 用「同步器令牌模式」:服务端生成随机 token 同时写入 cookie 和表单,提交时比对两者。攻击者无法读到目标站的 cookie,所以伪造不了 token。

  • Form 和 ModelForm 的区别?什么时候用哪个? Form 是通用表单(如登录、搜索、联系表单),字段手动定义。ModelForm 绑定到 Model,自动生成字段,save() 直接入库。CRUD 场景用 ModelForm,非数据库场景用 Form。

  • Django 的 XSS 防护机制? 模板默认对 {{ }} 输出做 HTML 转义。{% autoescape off %} 和 |safe 会关闭转义,慎用。富文本用户输入用 bleach 库做白名单过滤。响应头加 X-XSS-Protection、Content-Security-Policy。

自测三问

  1. 为什么 ModelForm 的 save(commit=False) 常用?给一个具体场景。
  2. CSRF token 在表单和 cookie 中各起什么作用?为什么必须两处都有?
  3. 如何只允许作者本人编辑自己的文章?写出 ModelForm + 视图的关键代码。

小结一句话

Django 表单系统把「接收 → 校验 → 清洗 → 反馈」做成流水线,配合 CSRF、XSS 防护和 messages 框架,让用户输入处理既安全又优雅。

延伸

  • Django 官方文档 - 表单
  • Django 官方文档 - 安全
  • 下一课:DRF 与异步任务——RESTful API、序列化、Celery 后台任务。
加载本节练习…
← 上一课Admin 与认证下一课 →DRF 与异步任务