タイトル: ajaxの使用例(POST編)
SEOタイトル: Django + Ajax POST 通信完全ガイド(CSRF Token / JsonResponse / fetch / DRF)
| この記事の要点 |
|
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 との比較
| 項目 | Django | Laravel Sanctum |
|---|---|---|
| CSRF | X-CSRFToken 必須 | X-XSRF-TOKEN 必須(SPA) |
| セッション認証 | 標準 | Sanctum::actingAs |
| API Token | DRF Token / JWT 追加 | 標準(Personal Access Token) |
| SPA テンプレート | Templates + 部分 Ajax | Vue/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 failed | X-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 error | views で例外 | 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'])。