Menu
CoddyTech

CSRF 和 API

本课属于 Coddy Django 学习旅程的“JSON API”单元。第 6 课,共 19 课。

一个真实的程序发送 POST /api/tasks/ 后会收到 403 Forbidden。Django 的 CSRF 保护要求每个 POST 请求都携带一个来自你自己某个页面的秘密令牌,这样其他网站就无法让访客的浏览器提交你的表单。程序没有这样的页面,也没有令牌。

来自 django.views.decorators.csrf 的 @csrf_exempt 会为一个视图关闭这项检查:

@csrf_exempt
def webhook(request):
    ...

这样做需要承担一些风险:任何网站现在都可以让访客的浏览器发送这个 POST 请求。对于不依赖用户登录状态的视图,例如任务列表,这样做没有问题。以已登录用户身份执行操作的 API 会保留 CSRF 保护,其页面脚本会在 X-CSRFToken 标头中发送令牌。

challenge icon

挑战

中等

允许不带 CSRF 令牌的程序创建任务。在 tasks/views.py 中,只对 task_list 豁免检查,这样 POST /api/tasks/ 就可以在没有令牌的情况下使用。

在 Shell 选项卡中运行下面这行代码。enforce_csrf_checks=True 会让测试 Client 检查令牌:它会输出 201,而不是 403。

from django.test import Client; print(Client(enforce_csrf_checks=True).post("/api/tasks/", {"title": "Pay rent"}, content_type="application/json").status_code)

亲手试试

import json

from django.http import JsonResponse
from django.shortcuts import render

from .models import Task


def task_to_dict(task):
    return {
        "id": task.id,
        "title": task.title,
        "done": task.done,
        "priority": task.priority,
    }


def home(request):
    return render(request, "tasks/home.html", {"tasks": Task.objects.all()})


def stats(request):
    data = {
        "total": Task.objects.count(),
        "done": Task.objects.filter(done=True).count(),
    }
    return JsonResponse(data)


def task_list(request):
    if request.method == "POST":
        data = json.loads(request.body)
        task = Task.objects.create(
            title=data["title"],
            priority=data.get("priority", "normal"),
        )
        return JsonResponse(task_to_dict(task), status=201)
    tasks = Task.objects.all()
    done = request.GET.get("done")
    if done == "true":
        tasks = tasks.filter(done=True)
    elif done == "false":
        tasks = tasks.filter(done=False)
    return JsonResponse([task_to_dict(task) for task in tasks], safe=False)


def task_detail(request, pk):
    task = Task.objects.filter(pk=pk).first()
    if task is None:
        return JsonResponse({"error": "Task not found"}, status=404)
    return JsonResponse(task_to_dict(task))

JSON API中的所有课

自己练习: Django 在线编辑器