17.

Django + Ajax POST 通信完全ガイド(CSRF Token / JsonResponse / fetch / DRF)

編集
この記事の要点
  • Django の Ajax POST は CSRF Token をヘッダ X-CSRFToken に必須で同送
  • Token 取得: テンプレート {% csrf_token %} または Cookie csrftoken
  • views.py で request.method == "POST" 判定、request.POST.get() / json.loads(request.body)
  • レスポンスは JsonResponse({...}) または DRF の @api_view + Response
  • 本格運用は Django REST Framework + SessionAuthentication / TokenAuthentication

Django + Ajax POST の全体像

Django で Ajax から POST する際、最大の関門は CSRF (Cross-Site Request Forgery) 保護です。Django は POST / PUT / PATCH / DELETE に対し毎リクエスト CSRF Tokenを要求します。Token を付け忘れると HTTP 403 Forbidden で弾かれます。

全体フロー

[ 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 をテンプレートに埋め込む

{# 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

// 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 不使用)

// 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)

# 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)で受ける場合

@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 を自動設定(境界文字列付き)
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 が無いとどうなるか

# レスポンス
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)で書く

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 との比較

# 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 は CSRF を要求します(ブラウザから叩く時)。TokenAuthentication / JWT は CSRF 不要。

Laravel Sanctum との比較

項目DjangoLaravel Sanctum
CSRFX-CSRFToken 必須X-XSRF-TOKEN 必須(SPA)
セッション認証標準Sanctum::actingAs
API TokenDRF Token / JWT 追加標準(Personal Access Token)
SPA テンプレートTemplates + 部分 AjaxVue/React + Inertia

セキュリティ設定

# 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 から取得 → header にセット
POST が GET 扱いされるmethod: 'post' なのに body 未送信Content-Type 設定 + body 確認
CORS エラークロスオリジンdjango-cors-headers 導入
JSON が空request.POST で取得json.loads(request.body) を使う
500 internal errorviews で例外DEBUG=True で原因確認

FAQ

Q: SPA から Ajax POST する時の CSRF はどうする?
A: ログイン後に Cookie csrftoken が払い出されます。JS で読み X-CSRFToken ヘッダに付ければ通ります。CORS では credentials: 'include'

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

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

編集
Post Share
子ページ

子ページはありません

同階層のページ
  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用コマンド・ファイルの作成

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