タイトル: ajaxの使用例(POST編)
SEOタイトル: Django + Ajax POST 通信完全ガイド(CSRF Token / JsonResponse / fetch / DRF)
この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li>Django の Ajax POST は <strong>CSRF Token</strong> をヘッダ <code>X-CSRFToken</code> に必須で同送</li><li>Token 取得: テンプレート <strong><code>{% csrf_token %}</code></strong> または Cookie <code>csrftoken</code></li><li>views.py で <code>request.method == "POST"</code> 判定、<strong><code>request.POST.get()</code></strong> / <code>json.loads(request.body)</code></li><li>レスポンスは <strong><code>JsonResponse({...})</code></strong> または DRF の <code>@api_view + Response</code></li><li>本格運用は <strong>Django REST Framework</strong> + <code>SessionAuthentication</code> / <code>TokenAuthentication</code></li></ul></td></tr></table>
<h2>Django + Ajax POST の全体像</h2>
<p>Django で Ajax から POST する際、最大の関門は <strong>CSRF (Cross-Site Request Forgery) 保護</strong>です。Django は POST / PUT / PATCH / DELETE に対し<strong>毎リクエスト CSRF Token</strong>を要求します。Token を付け忘れると HTTP 403 Forbidden で弾かれます。</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-text">全体フロー
[ 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 } │
│ ◀────────────────────────────────────│</code></pre>
<h2>手順1: CSRF Token をテンプレートに埋め込む</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html">{# 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></code></pre>
<h2>手順2: jQuery $.ajax での POST</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 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);
}
});</code></pre>
<h2>手順3: fetch API(jQuery 不使用)</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 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);
}
});</code></pre>
<h2>手順4: Django views.py で受ける(FBV)</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-python"># 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'),
]</code></pre>
<h3>FormData(multipart)で受ける場合</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-python">@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})</code></pre><pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// 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') },
});</code></pre>
<h2>CSRF Token が無いとどうなるか</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-text"># レスポンス
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):
...</code></pre>
<h2>CBV(Class-Based View)で書く</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-python">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()),</code></pre>
<h2>Django REST Framework との比較</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-python"># 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</code></pre>
<p>DRF の場合、デフォルトの <code>SessionAuthentication</code> は CSRF を要求します(ブラウザから叩く時)。<code>TokenAuthentication</code> / <code>JWT</code> は CSRF 不要。</p>
<h2>Laravel Sanctum との比較</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e8f5e9"><th>項目</th><th>Django</th><th>Laravel Sanctum</th></tr></thead>
<tbody>
<tr><td>CSRF</td><td><code>X-CSRFToken</code> 必須</td><td><code>X-XSRF-TOKEN</code> 必須(SPA)</td></tr>
<tr><td>セッション認証</td><td>標準</td><td><code>Sanctum::actingAs</code></td></tr>
<tr><td>API Token</td><td>DRF Token / JWT 追加</td><td>標準(Personal Access Token)</td></tr>
<tr><td>SPA テンプレート</td><td>Templates + 部分 Ajax</td><td>Vue/React + Inertia</td></tr>
</tbody>
</table>
<h2>セキュリティ設定</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-python"># 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</code></pre>
<h2>よくあるトラブル</h2>
<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#fff3e0"><th>症状</th><th>原因</th><th>対処</th></tr></thead>
<tbody>
<tr><td>403 Forbidden CSRF failed</td><td>X-CSRFToken 未送信 / Token 不一致</td><td>Cookie から取得 → header にセット</td></tr>
<tr><td>POST が GET 扱いされる</td><td><code>method: 'post'</code> なのに body 未送信</td><td><code>Content-Type</code> 設定 + <code>body</code> 確認</td></tr>
<tr><td>CORS エラー</td><td>クロスオリジン</td><td>django-cors-headers 導入</td></tr>
<tr><td>JSON が空</td><td><code>request.POST</code> で取得</td><td><code>json.loads(request.body)</code> を使う</td></tr>
<tr><td>500 internal error</td><td>views で例外</td><td>DEBUG=True で原因確認</td></tr>
</tbody>
</table>
<h2>FAQ</h2>
<p><strong>Q: SPA から Ajax POST する時の CSRF はどうする?</strong><br />
A: ログイン後に Cookie <code>csrftoken</code> が払い出されます。JS で読み <code>X-CSRFToken</code> ヘッダに付ければ通ります。CORS では <code>credentials: 'include'</code>。</p>
<p><strong>Q: <code>@csrf_exempt</code> はどんな時に使う?</strong><br />
A: 外部からの Webhook 受信(Stripe / GitHub 等)で署名検証を別に行う場合。<strong>通常の自社フロントには使わない</strong>でください。</p>
<p><strong>Q: ファイル + JSON を同時に送りたい</strong><br />
A: <code>FormData</code> でフィールドを追加し、JSON はテキスト化して <code>fd.append('meta', JSON.stringify(data))</code>。Django 側で <code>json.loads(request.POST['meta'])</code>。</p>