You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Django技术问询:结合表单与模型生成安全表单的实现疑问

解决Django工单表单安全验证问题:结合Form.is_valid()与cleaned_data

我来帮你梳理如何用Django原生表单机制安全实现这个需求,完全不用跳过自带的安全防护。核心思路是用Django表单类封装字段和验证逻辑,而不是手动在模板生成表单后自己处理POST数据。

场景1:单个工单选择+数量提交

如果用户只需要选择一个工单并填写对应数量,用普通表单类即可:

1. 定义表单类(包含验证逻辑)

from django import forms
from .models import Ticket

class TicketSelectionForm(forms.Form):
    # 自动从数据库拉取工单选项,安全生成下拉框/单选按钮
    ticket = forms.ModelChoiceField(
        queryset=Ticket.objects.all(),
        empty_label="请选择工单",
        label="选择工单"
    )
    # 内置基础验证:数量必须≥1
    quantity = forms.IntegerField(
        min_value=1,
        label="申请数量",
        widget=forms.NumberInput(attrs={"placeholder": "请输入数量"})
    )

    # 自定义业务规则验证:数量不超过工单可用库存
    def clean(self):
        cleaned_data = super().clean()
        selected_ticket = cleaned_data.get("ticket")
        requested_quantity = cleaned_data.get("quantity")

        if selected_ticket and requested_quantity:
            if requested_quantity > selected_ticket.available_quantity:
                raise forms.ValidationError(
                    f"工单「{selected_ticket.name}」可用数量不足,当前剩余{selected_ticket.available_quantity}份"
                )
        return cleaned_data

2. 视图处理请求

from django.shortcuts import render, redirect
from .forms import TicketSelectionForm

def ticket_request(request):
    if request.method == "POST":
        form = TicketSelectionForm(request.POST)
        if form.is_valid():
            # 从cleaned_data获取安全的验证后数据
            ticket = form.cleaned_data["ticket"]
            quantity = form.cleaned_data["quantity"]
            # 这里写你的业务逻辑:比如创建申请记录、扣减库存等
            # ...
            return redirect("request_success")
    else:
        # GET请求初始化空表单
        form = TicketSelectionForm()
    
    return render(request, "ticket_request.html", {"form": form})

3. 模板渲染(支持手动遍历工单选项)

如果不想用默认的下拉框,想手动渲染每个工单为单选按钮,模板可以这么写:

<form method="post">
    {% csrf_token %} <!-- 必须加,Django自动CSRF防护 -->
    
    <!-- 显示全局验证错误 -->
    {% if form.non_field_errors %}
        <div class="error">{{ form.non_field_errors }}</div>
    {% endif %}

    <!-- 手动遍历工单选项 -->
    <div>
        <label>选择工单:</label>
        {% for radio in form.ticket %}
            <div class="ticket-option">
                {{ radio.tag }} {{ radio.choice_label }}
            </div>
        {% endfor %}
        {% if form.ticket.errors %}
            <div class="error">{{ form.ticket.errors }}</div>
        {% endif %}
    </div>

    <!-- 数量输入框 -->
    <div>
        {{ form.quantity.label_tag }}
        {{ form.quantity }}
        {% if form.quantity.errors %}
            <div class="error">{{ form.quantity.errors }}</div>
        {% endif %}
    </div>

    <button type="submit">提交申请</button>
</form>

场景2:批量提交多个工单的数量

如果需要同时为多个工单填写数量并提交,用**表单集(FormSet)**实现:

1. 定义单个工单的表单单元

from django import forms
from .models import Ticket

class TicketQuantityForm(forms.Form):
    # 隐藏字段传递工单ID,避免手动输入的风险
    ticket = forms.ModelChoiceField(
        queryset=Ticket.objects.all(),
        widget=forms.HiddenInput()
    )
    # 允许不填数量(表示不申请该工单),但填了必须≥1
    quantity = forms.IntegerField(
        min_value=1,
        required=False,
        label="申请数量",
        widget=forms.NumberInput(attrs={"placeholder": "0表示不申请"})
    )

    def clean(self):
        cleaned_data = super().clean()
        ticket = cleaned_data.get("ticket")
        quantity = cleaned_data.get("quantity")

        if quantity is not None and quantity > 0:
            if quantity > ticket.available_quantity:
                raise forms.ValidationError(
                    f"工单「{ticket.name}」可用数量不足,剩余{ticket.available_quantity}份"
                )
        return cleaned_data

2. 视图中初始化表单集

from django.forms import formset_factory
from django.shortcuts import render, redirect
from .forms import TicketQuantityForm
from .models import Ticket

def bulk_ticket_request(request):
    # 创建表单集,extra=0表示不生成额外空表单
    TicketQuantityFormSet = formset_factory(TicketQuantityForm, extra=0)
    all_tickets = Ticket.objects.all()

    if request.method == "POST":
        formset = TicketQuantityFormSet(request.POST)
        if formset.is_valid():
            # 遍历每个表单处理数据
            for form in formset:
                ticket = form.cleaned_data.get("ticket")
                quantity = form.cleaned_data.get("quantity")
                if quantity and quantity > 0:
                    # 处理业务逻辑:创建申请记录等
                    # ...
            return redirect("bulk_request_success")
    else:
        # 用所有工单初始化表单集
        initial_data = [{"ticket": ticket} for ticket in all_tickets]
        formset = TicketQuantityFormSet(initial=initial_data)
    
    # 把工单和对应表单配对,方便模板渲染
    ticket_form_pairs = zip(all_tickets, formset)
    return render(request, "bulk_ticket_request.html", {
        "ticket_form_pairs": ticket_form_pairs,
        "formset": formset
    })

3. 批量提交模板

<form method="post">
    {% csrf_token %}
    {{ formset.management_form }} <!-- 表单集必须的管理字段,不可省略 -->
    
    {% for ticket, form in ticket_form_pairs %}
        <div class="ticket-item">
            <span>{{ ticket.name }} (可用数量: {{ ticket.available_quantity }})</span>
            {{ form.ticket }} <!-- 隐藏字段,传递工单ID -->
            {{ form.quantity.label_tag }}
            {{ form.quantity }}
            {% if form.errors %}
                <div class="error">{{ form.errors }}</div>
            {% endif %}
        </div>
    {% endfor %}

    <button type="submit">批量提交</button>
</form>

关键安全保障要点

  • CSRF防护:模板中必须添加{% csrf_token %},Django自动拦截跨站伪造请求
  • 字段验证:利用Django表单字段的内置验证(如min_value)和自定义clean方法,避免非法数据进入业务逻辑
  • cleaned_data:验证通过后仅从cleaned_data获取数据,这些数据已被清洗(比如转成正确的Python类型),避免直接操作request.POST的风险
  • ModelChoiceField:自动从数据库拉取工单选项,避免手动生成HTML选项导致的注入风险
  • 表单集管理字段:批量提交时必须渲染{{ formset.management_form }},确保表单集能正确识别POST数据

内容的提问来源于stack exchange,提问作者user9252255

火山引擎 最新活动