DjangoでAjax POSTを実装する例です。同一オリジンのセッション認証を対象に、送信先・CSRF・JSON入力・所有者の操作範囲を分けます。DRFを使う場合も認可は自動完成しません。
実装前に決めること
この教材では、登録済みユーザーが自分の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・重複/不正対策・結果不明時の扱いが別途必要です。失敗時の自動リトライは追加していません。
| この記事の要点 |
|
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 との比較
| 項目 | Django | Laravel Sanctum |
|---|---|---|
| CSRF | セッション方式等の条件を確認。ヘッダは送信方法の一つ | X-XSRF-TOKEN 必須(SPA) |
| セッション認証 | 標準 | SPAはセッションCookie。Sanctum::actingAsはテスト用の認証補助 |
| API Token | DRF Token / JWT 追加 | 標準(Personal Access Token) |
| SPA テンプレート | Templates + 部分 Ajax | UI/描画方式は別の選択。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 failed | X-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 error | views で例外 | 本番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'])。
失敗時に確認する順序
| 結果 | 確認点 |
|---|---|
| 403 | session認証、permission、CSRFのtoken/Origin/Refererを分ける。保護を外さない。 |
| 400/415 | JSONの構文/最上位object/項目/型/長さ/Content-Type。フォームPOSTとJSONを混同しない。 |
| 404 | URLか、所有者で絞った対象がないのか。別ユーザーの存在を明かす応答に変えない。 |
| 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のテスト用認証。
子ページはありません
- 環境構築とプロジェクト/アプリの作成
- MVC(MVT)のそれぞれの使い方と説明
- データベースへの接続と操作
- Django Administration
- git管理
- エラー一覧
- バージョンの確認方法
- ログ出力方法
- SQLのログ出力方法
- ログのローテート設定
- settings.pyの定数にアクセスする方法
- 本番環境へのインストールとアプリのデプロイ(apache編)
- 本番環境へのインストールとアプリのデプロイ(nginx編)
- djangoアプリの本番の開始URLを変更する
- 静的(static)ファイルの置き場所と読み込み(画像、css、js )
- CSRFトークンをAjaxで使用する方法
- ajaxの使用例(POST編)
- ファイルのアップロードとファイルの名前
- クイックスタート/チュートリアル
- ログイン機能
- テンプレート側のログイン判定
- ビュー側のログイン判定
- 管理者ユーザーの作成/判定と管理画面
- モデルのjson化とレスポンス
- runserverでポートを指定する方法
- cronによるバッチ実行
- テンプレートで利用する共通のcontextを定義する方法
- プログラムが本番サーバーで反映されない場合の対処法
- APIの作成
- cron用コマンド・ファイルの作成
人気ページ
- 1 Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法
- 2 tomcat の起動 / 停止ログと catalina.log・catalina.out の違い
- 3 JavaScript で base URL を取得する方法|window.location.origin
- 4 YouTube Data API v3 エラー一覧|403・400・404 の原因と対処
- 5 Laravel エラー一覧|500/Blade/DB 接続/ルーティングの代表エラー
- 6 3Dグラフィックスのソフト一覧|Blender・Metasequoiaと用途別の選び方
- 7 Spring Frameworkのアノテーション一覧
- 8 【Spring】@Valueアノテーションとは
- 9 CATALINA_HOME の確認方法 (Linux / Mac)
- 10 【Spring】@Autowiredアノテーションとは
最近更新/作成されたページ
- Visual Studio「デザイナーを表示できません」エラーの原因と対処 2026-10-28 12:06:00
- SSH 認証に失敗しました。再試行してください の対処 2026-10-28 12:06:00
- SSH2 秘密鍵の読み込みに失敗しました の原因と対処 2026-10-28 12:06:00
- TeraTerm のよくあるエラーと対処法完全ガイド — Connection refused / 2026-10-28 12:06:00
- Visual Studio エラー一覧|コンパイルエラー・警告の確認方法と対処 2026-10-28 11:39:57
- Whitelabel Error Page の原因と消し方|Spring Boot のエラーページ 2026-10-28 11:39:57
- Eclipseで「サーバーに追加または除去できるリソースがありません。」の原因と対処法 2026-10-28 11:39:57
- SQL WHERE 句完全ガイド 2026-10-28 11:39:57
- SQL JOIN 完全ガイド (INNER/LEFT/RIGHT/FULL OUTER) 2026-10-28 11:39:57
- tomcat の起動 / 停止ログと catalina.log・catalina.out の違い 2026-10-28 10:24:23
- Tomcat の起動確認方法 5 つ|localhost:8080・ポート・プロセス・ログ 2026-10-28 10:24:23
- Oracle 19c COMMITの使い方: 暗黙コミット・自動確定・応答不明時の確認 2026-10-28 09:59:09
- Oracle 19c DELETE文|削除対象の確認・COMMIT前の判断・ROLLBACKの条件 2026-10-28 09:59:09
- Oracle 19c INSERT文|INSERT ALL・SELECT・RETURNINGとMERGEの使い分け 2026-10-28 09:59:09
- Oracle 19cのトランザクション制御|COMMIT・ROLLBACK・分離レベルと接続設定 2026-10-28 09:59:09