◀ 17.

DjangoのAjax POST|CSRF・JSON入力検証・DRFの認可

▶

DjangoでAjax POSTを実装する例です。同一オリジンのセッション認証を対象に、送信先・CSRF・JSON入力・所有者の操作範囲を分けます。DRFを使う場合も認可は自動完成しません。

Ajax POSTで同一オリジン、sessionとCSRF、入力とownerを別々に検査する図

実装前に決めること

この教材では、登録済みユーザーが自分のContactだけを作成・一覧・詳細・更新・削除できる方針に限定します。管理者でも他人の行を扱う特例はありません。Contactのownerをブラウザから受け取らず、認証したrequest.userで確定します。これは業務ごとのモデル権限・組織管理・公開問い合わせフォームの設計を代行するものではありません。

既存アプリへ設定全体をコピーしたり、モデルを差し替えたりしないでください。以降の新例は専用の隔離教材のファイルです。実装を移す場合は既存のauth/session/CSRF middlewareとアプリ登録・URL接続を確認し、DB設計変更は別の承認済み手順で扱います。

元の閲覧カウント例を安全に読み直す

初期版はリンククリック時に閲覧数をAjaxで増やす例でした。元のpageというモデル名と変数名の上書き、PageCountupView/pageCountupViewの綴り違い、未送信page_idの直接参照、読んだviewsへ1を足して保存する処理は、そのまま復活させません。新教材は所有者が自分の合成カウンターだけを更新する限定例です。

counter.pyは単独ファイルではなく、「手順4」のapi.pyのJSON検査と、「認証・所有者・入力fieldを明示する例」のPageCounterモデル/urls.pyを使います。POST /api/page/countup/へ整数idを送る例で、後述のContact送信フォームとは別の用途です。数値上限に達した行/対象外は更新せず、同じ404を返します。

from django.db.models import F
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_POST

from .api import InputError, read_django_json
from .models import PageCounter


@require_POST
@csrf_protect
def increment_counter(request):
    if not request.user.is_authenticated:
        return JsonResponse({"detail": "Login required."}, status=403)
    try:
        data = read_django_json(request)
    except InputError as exc:
        return JsonResponse({"detail": str(exc)}, status=exc.status)
    if (not isinstance(data, dict) or set(data) != {"id"}
            or type(data["id"]) is not int or not 1 <= data["id"] <= 2147483647):
        return JsonResponse({"detail": "Expected one positive integer id."}, status=400)
    # One scoped UPDATE; cap at the portable PositiveIntegerField upper bound.
    changed = PageCounter.objects.filter(
        pk=data["id"], owner=request.user, views__lt=2147483647,
    ).update(views=F("views") + 1)
    if not changed:
        return JsonResponse({"detail": "Unavailable counter."}, status=404)
    return JsonResponse({"ok": True})

F式によるDB側の加算は、読取後に同じ値を上書きする問題を避けるためです。正しい閲覧数・一度だけの計上・不正連打防止を保証しません。クリックとページ閲覧も別で、タブ移動/通信失敗/再送で計上結果は変わります。公開カウンターや報酬に使う前には対象の公開条件・rate limit・重複/不正対策・結果不明時の扱いが別途必要です。失敗時の自動リトライは追加していません。

この記事の要点
  • 同一オリジンのセッション認証では、認可・入力検証に加えCSRF保護を行う。AjaxではX-CSRFTokenも使える
  • Token 取得: テンプレート {% csrf_token %} または Cookie csrftoken
  • views.py で request.method == "POST" 判定、request.POST.get() / json.loads(request.body)
  • レスポンスは JsonResponse({...}) または DRF の @api_view + Response
  • DRF採用だけで安全にはならない。認証方式・permission_classes・所有者queryset・入力fieldを明示する

Django + Ajax POST の全体像

同一オリジンのAjax POSTでも、認証・操作権限・入力・CSRFを別々に確認します。標準CSRF middlewareが有効な通常のDjangoビューではunsafe methodを検査します。DRFのSessionAuthenticationは認証済みsessionでCSRFを検査するため、匿名を拒否するpermissionが別に必要です。トークン成功だけで他人の行を変更してよいとはなりません。

旧例の注意:これは同一オリジン・セッション認証の概略です。実際にはCSRFだけでなくログイン・対象の操作権限・入力検証も必要で、成功statusは実装の契約に従います。

全体フロー

[ Browser ]                            [ Django ]
   │                                       │
   │  GET /form/  (HTML + csrftoken Cookie) │
   │ ◀────────────────────────────────────│
   │                                       │
   │  POST /api/save/                       │
   │   X-CSRFToken: <token>                 │
   │   Content-Type: application/json       │
   │   { "name": "Taro" }                   │
   │ ────────────────────────────────────▶ │
   │                                       │ ← CSRF middleware が検証
   │                                       │ ← views.py で処理
   │  200 { "ok": true, "id": 42 }          │
   │ ◀────────────────────────────────────│

手順1: CSRF Token をテンプレートに埋め込む

同一オリジンの送信例

フォームのmethod/actionを明示し、CSRFトークンはDOMから取得します。name属性をフォーム自身と混同しないようelements.namedItemで取り出します。出力はtextContentへ固定メッセージを設定し、メールや応答全文をログへ出しません。送信中のbutton無効化は画面操作の補助で、サーバーの重複排除ではありません。

{% load static %}
<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>連絡先の登録</title></head>
<body>
  <form id="contact-form" method="post" action="/api/save/">
    {% csrf_token %}
    <label>名前 <input name="name" required maxlength="80" autocomplete="name"></label>
    <label>メールアドレス <input name="email" type="email" required maxlength="254" autocomplete="email"></label>
    <button type="submit">保存</button>
  </form>
  <p id="contact-status" role="status" aria-live="polite"></p>
  <p>このJSON専用の送信先にはJavaScriptが必要です。</p>
  <script src="{% static 'django-ajax-post/client.js' %}" defer></script>
</body>
</html>
"use strict";

const form = document.getElementById("contact-form");
const statusNode = document.getElementById("contact-status");
const submitButton = form.querySelector('button[type="submit"]');
let submitting = false;

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  if (submitting) return;
  submitting = true;
  submitButton.disabled = true;
  statusNode.textContent = "送信中...";
  try {
    const token = form.querySelector('[name="csrfmiddlewaretoken"]').value;
    if (!token) throw new Error("Missing token");
    const response = await fetch("/api/save/", {
      method: "POST",
      mode: "same-origin",
      credentials: "same-origin",
      redirect: "error",
      headers: {
        "Content-Type": "application/json; charset=UTF-8",
        "Accept": "application/json",
        "X-CSRFToken": token,
      },
      body: JSON.stringify({
        name: form.elements.namedItem("name").value,
        email: form.elements.namedItem("email").value,
      }),
    });
    if (response.status === 201) {
      const mediaType = (response.headers.get("Content-Type") || "").split(";")[0].trim().toLowerCase();
      if (mediaType !== "application/json") throw new Error("Unexpected response type");
      const result = await response.json();
      if (!result || typeof result !== "object" || Array.isArray(result)
          || !Number.isSafeInteger(result.id) || result.id <= 0) {
        throw new Error("Unexpected response shape");
      }
      statusNode.textContent = "保存しました。";
    } else if (response.status === 400 || response.status === 413 || response.status === 415) {
      statusNode.textContent = "保存されませんでした。入力内容と送信形式を確認してください。";
    } else if (response.status === 403) {
      statusNode.textContent = "保存されませんでした。ログイン状態を確認し、フォームを再読込してください。";
    } else {
      statusNode.textContent = "保存結果が不明です。再送する前に保存済みの連絡先を確認してください。";
    }
  } catch {
    // A network/redirect failure does not prove that the server did not save.
    statusNode.textContent = "保存結果が不明です。保存済みの連絡先を確認し、再読込してください。";
  } finally {
    submitting = false;
    submitButton.disabled = false;
  }
});

旧例の注意:旧フォームは説明用断片です。method/actionや必須入力を省略しており、JSが動かないと既定のGETで個人情報がURLへ出る恐れがあります。実用には「同一オリジンの送信例」のフォームを使い、コメント内のcsrf_tokenもテンプレートでは展開される点に注意します。

{# templates/form.html #}
<form id="myForm">
    {% csrf_token %}
    <input name="name">
    <input name="email">
    <button type="submit">送信</button>
</form>

<script>
// {% csrf_token %} は次のような hidden input を生成
// <input type="hidden" name="csrfmiddlewaretoken" value="abc123...">
</script>

手順2: jQuery $.ajax での POST

旧例の注意:グローバルajaxSetupをそのまま使いません。この旧例は外部の送信先へCSRFトークンを送る制限がなく、Cookie読取りにも設定条件があります。個別の同一オリジンrequestに限定し、応答本文・氏名・メールをconsoleへ出しません。

// CSRF Token を Cookie から取得
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}
const csrftoken = getCookie('csrftoken');

// $.ajaxSetup でグローバルに設定(推奨)
$.ajaxSetup({
    beforeSend: function (xhr, settings) {
        if (!/^(GET|HEAD|OPTIONS|TRACE)$/.test(settings.type)) {
            xhr.setRequestHeader('X-CSRFToken', csrftoken);
        }
    }
});

// POST 実行
$.ajax({
    method: 'POST',
    url: '/api/save/',
    contentType: 'application/json',
    data: JSON.stringify({
        name: $('input[name=name]').val(),
        email: $('input[name=email]').val(),
    }),
    success: function (resp) {
        console.log('OK', resp);
    },
    error: function (xhr) {
        console.error('NG', xhr.status, xhr.responseText);
    }
});

手順3: fetch API(jQuery 不使用)

旧例の注意:旧postJSONは任意URLへのトークン送信やredirectを制限していません。e.target.nameはフォーム自身のnameと衝突し得るためelements.namedItemを使います。応答本文や例外全体のログ出力・失敗時の自動再送を避けます。

// CSRF Token 取得
function getCookie(name) {
    const m = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
    return m ? decodeURIComponent(m.pop()) : '';
}

async function postJSON(url, data) {
    const res = await fetch(url, {
        method: 'POST',
        credentials: 'same-origin',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCookie('csrftoken'),
            'X-Requested-With': 'XMLHttpRequest',
        },
        body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
}

// 利用
document.getElementById('myForm').addEventListener('submit', async (e) => {
    e.preventDefault();
    try {
        const json = await postJSON('/api/save/', {
            name: e.target.name.value,
            email: e.target.email.value,
        });
        console.log('OK', json);
    } catch (err) {
        console.error(err);
    }
});

手順4: Django views.py で受ける(FBV)

新しいapi.pyには通常ビューとDRFクラスをまとめています。JSONの形式と項目を検証してから保存し、本文は4096byteまで、UTF-8のみとする教材です。CSRFの拒否はJSONの400より先になることがあります。通常Django版とDRF版は別のURLへ接続し、同じURLに両方を登録しません。対応するmodels.py/urls.pyは「認証・所有者・入力fieldを明示する例」にあります。

import json

from django import forms
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_protect
from django.views.decorators.http import require_GET, require_POST
from rest_framework import serializers, viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import APIException, ParseError, UnsupportedMediaType
from rest_framework.pagination import PageNumberPagination
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer

from .models import Contact


MAX_JSON_BYTES = 4096


class InputError(ValueError):
    def __init__(self, status, message):
        self.status = status
        super().__init__(message)


def reject_constant(value):
    raise ValueError("Non-finite JSON number")


def read_json(stream, encoding):
    if (encoding or "utf-8").lower().replace("-", "") != "utf8":
        raise InputError(415, "Use UTF-8 JSON.")
    raw = stream.read(MAX_JSON_BYTES + 1)
    if len(raw) > MAX_JSON_BYTES:
        raise InputError(413, "JSON body too large.")
    try:
        return json.loads(raw.decode("utf-8"), parse_constant=reject_constant)
    except (UnicodeDecodeError, ValueError, RecursionError):
        raise InputError(400, "Invalid JSON.") from None


def read_django_json(request):
    if request.content_type != "application/json":
        raise InputError(415, "Use application/json.")
    return read_json(request, request.encoding)


def contact_input_error(data):
    if not isinstance(data, dict):
        return "Expected a JSON object."
    if set(data) - {"name", "email"}:
        return "Only name and email may be submitted."
    if any(type(value) is not str for value in data.values()):
        return "Field values must be strings."
    return None


class ContactForm(forms.ModelForm):
    class Meta:
        model = Contact
        fields = ("name", "email")


@login_required
@require_GET
def contact_form(request):
    return render(request, "form.html")


@require_POST
@csrf_protect
def create_contact(request):
    if not request.user.is_authenticated:
        return JsonResponse({"detail": "Login required."}, status=403)
    try:
        data = read_django_json(request)
    except InputError as exc:
        return JsonResponse({"detail": str(exc)}, status=exc.status)
    error = contact_input_error(data)
    if error:
        return JsonResponse({"detail": error}, status=400)
    form = ContactForm(data)
    if not form.is_valid():
        return JsonResponse({"errors": form.errors.get_json_data()}, status=400)
    contact = form.save(commit=False)
    contact.owner = request.user
    contact.save()
    return JsonResponse({"id": contact.pk}, status=201)


class BodyTooLarge(APIException):
    status_code = 413
    default_detail = "JSON body too large."


class BoundedJSONParser(JSONParser):
    def parse(self, stream, media_type=None, parser_context=None):
        try:
            return read_json(stream, (parser_context or {}).get("encoding"))
        except InputError as exc:
            if exc.status == 413:
                raise BodyTooLarge() from None
            if exc.status == 415:
                raise UnsupportedMediaType(media_type) from None
            raise ParseError("Invalid JSON.") from None


class ContactSerializer(serializers.ModelSerializer):
    class Meta:
        model = Contact
        fields = ("id", "name", "email")
        read_only_fields = ("id",)

    def to_internal_value(self, data):
        # DRF normally ignores read-only/unknown inputs and coerces some numbers.
        error = contact_input_error(data)
        if error:
            raise serializers.ValidationError({"detail": error})
        return super().to_internal_value(data)


class ContactPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = "page_size"
    max_page_size = 50


class ContactViewSet(viewsets.ModelViewSet):
    authentication_classes = (SessionAuthentication,)
    permission_classes = (IsAuthenticated,)
    parser_classes = (BoundedJSONParser,)
    renderer_classes = (JSONRenderer,)
    serializer_class = ContactSerializer
    pagination_class = ContactPagination
    http_method_names = ["get", "post", "put", "patch", "delete", "head", "options"]

    def get_queryset(self):
        # This scope also applies to retrieve/update/destroy, returning 404 outside it.
        return Contact.objects.filter(owner=self.request.user).order_by("id")

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)

旧例の注意:この旧保存関数には認証/所有者の対応、Content-TypeとJSON最上位の型、文字列型・長さ・メール検証がありません。配列/数値や不正UTF-8で500になり得ます。require_POSTやCSRFだけで公開保存を許可せず、後述の検証済み教材と区別します。

# views.py
import json
from django.http import JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_POST

@require_POST
def api_save(request):
    # JSON ボディ
    try:
        payload = json.loads(request.body.decode('utf-8'))
    except json.JSONDecodeError:
        return HttpResponseBadRequest('invalid JSON')

    name  = (payload.get('name') or '').strip()
    email = (payload.get('email') or '').strip()

    if not name or not email:
        return JsonResponse({'ok': False, 'errors': {'name': '必須', 'email': '必須'}}, status=400)

    # DB 保存
    from .models import Contact
    obj = Contact.objects.create(name=name, email=email)
    return JsonResponse({'ok': True, 'id': obj.id})


# urls.py
from django.urls import path
from . import views
urlpatterns = [
    path('api/save/', views.api_save, name='api_save'),
]

FormData(multipart)で受ける場合

旧例の注意:この旧uploadは認可・容量・内容・保存場所・配信制御が不足しています。元のファイル名/MIMEを信用せず、名前を使った保存だけで安全とみなしません。本記事の新しいJSON教材にファイルの保存機能はなく、アップロードの個別記事で条件を確認します。

@require_POST
def api_upload(request):
    name = request.POST.get('name', '').strip()
    file = request.FILES.get('avatar')
    if not file:
        return JsonResponse({'ok': False, 'msg': 'file required'}, status=400)

    # ファイル保存
    from django.core.files.storage import default_storage
    path = default_storage.save(f'avatars/{file.name}', file)
    return JsonResponse({'ok': True, 'path': path})

旧例の注意:FormDataのContent-Typeはブラウザに境界を付けさせますが、これだけで認証やアップロード検証は完成しません。fileInput/getCookie等の定義も必要な旧断片で、今回ファイル送信は実行していません。

// FormData は Content-Type を自動設定(境界文字列付き)
const fd = new FormData();
fd.append('name', 'Taro');
fd.append('avatar', fileInput.files[0]);

await fetch('/api/upload/', {
    method: 'POST',
    body: fd,
    headers: { 'X-CSRFToken': getCookie('csrftoken') },
});

CSRF Token が無いとどうなるか

旧例の注意:csrf_exemptやSameSite=Noneを403の解決策として貼り付けません。Webhookは署名/再送/対象サービスの仕様を別途設計します。CookieをJSで読めない設定ではDOMのトークンを使い、設定を弱めて読み出しません。

# レスポンス
HTTP/1.1 403 Forbidden

<h1>Forbidden (403)</h1>
<p>CSRF verification failed. Request aborted.</p>

# settings.py
CSRF_COOKIE_HTTPONLY = False   # JS から読みたいなら False(既定)
CSRF_COOKIE_SAMESITE = 'Lax'   # クロスサイトでは 'None' + Secure

# 例外的に CSRF をスキップしたい view(外部 Webhook 等)
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook(request):
    ...

CBV(Class-Based View)で書く

旧例の注意:これは未完成CBVの旧断片で保存処理や認可を実装していません。JSONDecodeErrorだけでは最上位の型/入力/encodingを検査できず、ok=trueも保存の証明ではありません。FBVかCBVかという違いと安全性は別です。

from django.views import View
from django.http import JsonResponse
import json

class SaveAPI(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
        except json.JSONDecodeError:
            return JsonResponse({'ok': False, 'msg': 'invalid JSON'}, status=400)
        # ...
        return JsonResponse({'ok': True})

# urls.py
path('api/save/', SaveAPI.as_view()),

Django REST Framework との比較

認証・所有者・入力fieldを明示する例

モデルと、先のapi.py、次のURLを対応付けます。IsAuthenticatedは匿名を拒否するだけなので、get_querysetで所有者を限定します。一覧はobject permissionが各行へ自動適用されると期待せず、一覧と詳細・更新・削除を同じ範囲にします。作成はperform_createでownerを確定します。入力id/owner等の余計な項目も拒否する教材です。一覧は一意なid順で既定20/最大50件ですが、同時更新中のsnapshotを保証しません。

from django.conf import settings
from django.db import models


class Contact(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    name = models.CharField(max_length=80)
    email = models.EmailField(max_length=254)


class PageCounter(models.Model):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    views = models.PositiveIntegerField(default=0)
from django.urls import include, path
from rest_framework.routers import SimpleRouter

from .api import ContactViewSet, contact_form, create_contact
from .counter import increment_counter


router = SimpleRouter()
router.register("contacts", ContactViewSet, basename="contact")

urlpatterns = [
    path("form/", contact_form, name="contact-form"),
    path("api/save/", create_contact, name="contact-create"),
    path("api/page/countup/", increment_counter, name="page-countup"),
    path("api/", include(router.urls)),
]

旧例の注意:この旧ContactViewSetをそのまま公開しないでください。直前の関数saveに付いたIsAuthenticatedは別クラスに引き継がれません。ViewSetの認可指定がなく全体設定も既定ならAllowAnyで、全件querysetと全fieldのCRUDが公開され得ます。新例は明示認証/認可・所有者での絞込み・入力field固定を行います。

# api/views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated

@api_view(['POST'])
@permission_classes([IsAuthenticated])
def save(request):
    name  = request.data.get('name')
    email = request.data.get('email')
    return Response({'ok': True, 'name': name})


# ModelSerializer + ViewSet が本命
from rest_framework import viewsets, serializers
from .models import Contact

class ContactSerializer(serializers.ModelSerializer):
    class Meta:
        model = Contact
        fields = '__all__'

class ContactViewSet(viewsets.ModelViewSet):
    queryset = Contact.objects.all()
    serializer_class = ContactSerializer

# urls.py
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('contacts', ContactViewSet)
urlpatterns += router.urls

DRFのSessionAuthenticationは認証済みsessionのunsafe methodでCSRFを検査します。匿名requestがCSRFなしで処理され得るため、IsAuthenticatedを各APIへ明示します。Token/JWTという名称だけでCSRF不要とは判断せず、資格情報をブラウザが自動送信するCookieに入れる方式か、Authorizationヘッダへ明示する方式か、別の認証方式も有効かを確認します。今回Token/JWTやログインAPIの実装は検証していません。

Laravel Sanctum との比較

項目DjangoLaravel Sanctum
CSRFセッション方式等の条件を確認。ヘッダは送信方法の一つX-XSRF-TOKEN 必須(SPA)
セッション認証標準SPAはセッションCookie。Sanctum::actingAsはテスト用の認証補助
API TokenDRF Token / JWT 追加標準(Personal Access Token)
SPA テンプレートTemplates + 部分 AjaxUI/描画方式は別の選択。SanctumだけでInertia必須とはならない

セキュリティ設定

旧例の注意:設定一覧を丸ごと追加しません。SECURE_BROWSER_XSS_FILTERはDjango4.0で削除済みです。HSTS/HTTPS転送は既存TLS/プロキシ経路を確認して段階的に設計し、CSRF_TRUSTED_ORIGINSとCORS許可を同じ機能と扱わず、任意サイトを追加して403を隠しません。

# settings.py
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 60 * 60 * 24 * 365
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# 本番のみ
CSRF_TRUSTED_ORIGINS = ['https://app.example.com']
CORS_ALLOWED_ORIGINS = ['https://app.example.com']   # django-cors-headers

よくあるトラブル

症状原因対処
403 Forbidden CSRF failedX-CSRFToken 未送信 / Token 不一致Cookie/DOMの方式、トークン・Origin/Referer・TLS/proxyを確認。保護を無効化しない
POST が GET 扱いされる本文が空でもPOSTはPOST。redirectや送信先・methodを確認Networkで元requestとredirectを分ける。Content-Typeを付けるだけでは直らない
CORS エラークロスオリジンまず送信先とOriginを確認。同一オリジンならCORS追加は不要。credentials/CSRFは別の条件
JSON が空request.POST で取得json.loads(request.body) を使う
500 internal errorviews で例外本番DEBUGを有効化せず、許可された秘匿済みログで分類する

FAQ

Q: SPA から Ajax POST する時の CSRF はどうする?
A: トークンはテンプレートのcsrf_token等で準備し、ログイン時のsecret更新後は画面を更新して取得し直します。HttpOnly/session保存ではCookieをJSから読まずDOMから取得します。credentials: includeだけではCORS/CSRFの許可は成立しません。本記事は同一オリジン専用です。

Q: @csrf_exempt はどんな時に使う?
A: 外部からの Webhook 受信(Stripe / GitHub 等)で署名検証を別に行う場合。通常の自社フロントには使わないでください。

Q: ファイル + JSON を同時に送りたい
A: FormData でフィールドを追加し、JSON はテキスト化して fd.append('meta', JSON.stringify(data))。Django 側で json.loads(request.POST['meta'])。

失敗時に確認する順序

結果確認点
403session認証、permission、CSRFのtoken/Origin/Refererを分ける。保護を外さない。
400/415JSONの構文/最上位object/項目/型/長さ/Content-Type。フォームPOSTとJSONを混同しない。
404URLか、所有者で絞った対象がないのか。別ユーザーの存在を明かす応答に変えない。
405許可methodとURL。GETで保存しない。
network失敗/redirect/HTMLログイン画面や上流エラーをJSON成功とみなさない。処理が実行されたか不明なら自動再送しない。

FormDataはrequest.POST/request.FILES、application/jsonは通常ビューのrequest.bodyまたはDRFのparserで扱います。JSONが配列/数値のケース、メール/長さ/所有者の検査を省略しません。ファイル保存はDjangoアップロード、トークンの準備はAjaxのCSRF、ビュー構成はViewの個別ガイドも参照してください。

検証範囲と公式資料

2026年10月26日確認。Python3.12.10/Django5.2.18/DRF3.18.3/SQLite3.49.1の専用プロセスで、合成ユーザー/session/Contact/PageCounterを使う25試験を確認しました。実sessionとCSRF検査を有効にし、匿名・所有者・別人のCRUD、入力偽装/不正JSON、token/Origin、HttpOnly/session保存、ログイン後のtoken更新、カウンターを検査しています。JSはDOM/fetchの代替を使う13試験であり実ブラウザの通信試験ではありません。本番ログイン・POST・アップロード・DB/schema変更はしていません。実ブラウザ全環境、TLS/proxy/CORS/外部API/Token/JWT・並列更新・負荷・実業務認可の網羅は未検証です。旧比較例の保持はそのまま公開実装する推奨ではありません。

公式資料:Django CSRFとAjax、DRF認可の既定値とobject権限、所有者queryset、認証とCSRFの条件、F式、削除済み設定、Sanctumのテスト用認証。

子ページ

子ページはありません

同階層のページ
  1. 環境構築とプロジェクト/アプリの作成
  2. MVC(MVT)のそれぞれの使い方と説明
  3. データベースへの接続と操作
  4. Django Administration
  5. git管理
  6. エラー一覧
  7. バージョンの確認方法
  8. ログ出力方法
  9. SQLのログ出力方法
  10. ログのローテート設定
  11. settings.pyの定数にアクセスする方法
  12. 本番環境へのインストールとアプリのデプロイ(apache編)
  13. 本番環境へのインストールとアプリのデプロイ(nginx編)
  14. djangoアプリの本番の開始URLを変更する
  15. 静的(static)ファイルの置き場所と読み込み(画像、css、js )
  16. CSRFトークンをAjaxで使用する方法
  17. ajaxの使用例(POST編)
  18. ファイルのアップロードとファイルの名前
  19. クイックスタート/チュートリアル
  20. ログイン機能
  21. テンプレート側のログイン判定
  22. ビュー側のログイン判定
  23. 管理者ユーザーの作成/判定と管理画面
  24. モデルのjson化とレスポンス
  25. runserverでポートを指定する方法
  26. cronによるバッチ実行
  27. テンプレートで利用する共通のcontextを定義する方法
  28. プログラムが本番サーバーで反映されない場合の対処法
  29. APIの作成
  30. cron用コマンド・ファイルの作成

最近更新/作成されたページ