◀ 29.

Django REST FrameworkでAPIを作る|URL接続・入力検証・所有者の認可

▶

Django REST Frameworkで、モデル・serializer・ViewSet・URLを接続する例です。動くURLを作ることと、誰がどの行を操作できるかは別に確認します。旧コードの名前違いを直しただけで公開しないでください。

旧画面と新しい応答の違い

元の動作確認画像を先に示します。旧Browsable APIのHTTP200・id/user_idの一覧と入力欄を示す参考画像で、正確なDjango/DRFの版や権限は画像だけから確定できません。今回の新教材はJSON専用で、同じHTML画面や旧入力欄を再現するものではありません。

動作の確認の図

新教材の方針は「登録したユーザーが自分のReportだけを作成・一覧・詳細・更新・削除できる」です。UUIDはサーバーで生成し、ownerはrequest.userから設定します。ブラウザが渡すuser_id/id/ownerを信用しません。UUIDを知りにくくすることも認可の代わりにはなりません。

ファイル役割
api/models.py新教材のReport定義
api/serializers.pyReportと入力/出力fieldの対応
api/views.py認証/認可・所有者の範囲・作成時owner
api/urls.pyrouterとurlpatterns
api_project/urls.pyapi.urlsを/api/配下へ接続

次の新例は独立教材のファイル全体です。既存アプリのモデル/schemaや設定全体を置換しません。既存の認証したsessionとauth/session/CSRF middleware、対象アプリの登録・DBの準備が前提です。実アプリへ取り込むDB設計変更や配布は別に検討し、この確認のために本番migrationを実行しません。

前提

・djangoインストール済み。

・本稿ではWindows OSを例にしますが適宜読み替えて下さい。

・本稿では出来るだけAPIに関する部分のみを記述する。他に関しては別記事を参照すること。

・プロジェクト名を「api_project」とする

・適当なアプリを作成しておく。今回は「api」アプリとする。

 

必要ライブラリ

旧コード表の注意:これは旧導入コマンドの記録です。既存の本番環境へ無指定版を追加する指示ではありません。新教材はDjango5.2.18/DRF3.18.3で確認し、django-filterを使用しないため追加していません。importだけで検索や認可が有効になるわけではありません。

pip install djangorestframework

pip install django-filter

 

設定ファイル

旧コード表の注意:この2項目だけで既存INSTALLED_APPS全体を置き換えないでください。認証/sessionには既存のauth/contenttypes/sessions等とmiddlewareの接続が必要です。既存設定を保持して、対象アプリとrest_frameworkを適切に登録します。

api_project/settings.py

INSTALLED_APPS = (

    'api',

    'rest_framework',

)

 

モデルの作成

新教材は元の文字列id/user_idだけのモデルとは異なり、サーバー生成UUID、認証ユーザーへのowner、200文字以内のmessageです。ownerは応答へ出しません。

from uuid import uuid4

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


class Report(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    message = models.CharField(max_length=200)

旧コード表の注意:旧モデルはidとuser_idの2列を定義しており「idのみ」という説明は誤りです。このuser_idは単なる文字列で、認証したユーザーとの関係を保証しません。新教材のUUID/owner/messageは別のモデル定義で、既存DBの列をそのまま変更する指示ではありません。

api/models.py

from django.db import models


class Report(models.Model):

    id = models.CharField(max_length=200, primary_key=True)

    user_id = models.CharField(max_length=200)

※DB設定はAPI固有のものではないので省きます。

旧定義にはidとuser_idがあります。新しい教材では下記の別モデルを使い、ownerを認証ユーザーから設定します。

 

シリアライザーの作成

api/serializers.pyです。Meta.modelをReportに合わせ、応答はid/messageのみ。作成/PUTのmessageは空でない文字列を必須とし、余計なid/owner/user_id等の入力は拒否します。PATCHは部分更新です。

from rest_framework import serializers

from .models import Report


class ReportSerializer(serializers.ModelSerializer):
    class Meta:
        model = Report
        fields = ("id", "message")
        read_only_fields = ("id",)

    def to_internal_value(self, data):
        if not isinstance(data, dict) or set(data) - {"message"}:
            raise serializers.ValidationError({"detail": "Only message may be submitted."})
        if "message" in data and type(data["message"]) is not str:
            raise serializers.ValidationError({"message": "Expected a string."})
        return super().to_internal_value(data)

旧コード表の注意:この旧表のラベルはapi/models.pyですが、serializerは新例のapi/serializers.pyに分離します。定義したReportと、importのUser/Entry・Meta.model=Userが一致していません。旧fieldsはid/user_idの明示指定であり、全field指定ではありません。名前をReportへ直すだけでも入力user_idを認証/認可として信用してはいけません。

api/models.py

from rest_framework import serializers

from .models import User, Entry


class ReportSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'user_id')

※DB設定はAPI固有のものではないので省きます。

 

URLの設定

まずプロジェクトのapi_project/urls.py、次にアプリのapi/urls.pyを示します。前者は教材上project_urls.pyというファイル名で保存しています。既存プロジェクトでは他のルートを保持し、include対象と実ファイル名を照合します。

from django.urls import include, path

# Logical location: api_project/urls.py. Preserve other existing project routes.
urlpatterns = [path("api/", include("api.urls"))]
from rest_framework.routers import SimpleRouter

from .views import ReportViewSet

router = SimpleRouter()
# No class-level queryset: supply basename explicitly.
router.register("test", ReportViewSet, basename="report")
urlpatterns = router.urls

旧コード表の注意:標準的なプロジェクト側ファイル名はurls.pyです。includeの文字列api.urlsと実ファイル/モジュール名を合わせ、既存adminや他のURLをこの短い一覧で消さないでください。URLの接続だけで認証や許可は設定されません。

api_project/url.py

from django.contrib import admin

from django.urls import include, path

 

urlpatterns = [

    path('admin/', admin.site.urls),

    path('api/', include('api.urls')),

]
 

 

旧コード表の注意:この旧表はapi/url.pyと書かれていますが、include先はapi.urlsです。routerを作るだけでなくurlpatterns=router.urlsを公開する必要があります。新例はクラスのquerysetを使わないためbasenameを明示します。

api/url.py

from rest_framework import routers

from .views import TestView

 

router = routers.DefaultRouter()

router.register(r'test', TestView)

GET /api/test/を解決するには、プロジェクトのinclude、アプリのurlpatterns、ViewSetとserializer/モデルが正しく接続している必要があります。旧コードはそのままでは不一致があり、アクセス成功を断定できません。

 

ビューの作成

api/views.pyです。IsAuthenticatedだけで他人の行を保護せず、全操作のget_querysetをownerで限定し、作成時はperform_createでownerを確定します。管理者が他人の行を操作する例外・共有・業務のモデル権限は実装していません。

from io import BytesIO

from rest_framework.authentication import SessionAuthentication
from rest_framework.exceptions import APIException, 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 rest_framework.viewsets import ModelViewSet

from .models import Report
from .serializers import ReportSerializer


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):
        encoding = (parser_context or {}).get("encoding") or "utf-8"
        if encoding.lower().replace("-", "") != "utf8":
            raise UnsupportedMediaType(media_type)
        raw = stream.read(4097)
        if len(raw) > 4096:
            raise BodyTooLarge()
        return super().parse(BytesIO(raw), media_type, parser_context)


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


class ReportViewSet(ModelViewSet):
    authentication_classes = (SessionAuthentication,)
    permission_classes = (IsAuthenticated,)
    parser_classes = (BoundedJSONParser,)
    renderer_classes = (JSONRenderer,)
    serializer_class = ReportSerializer
    pagination_class = ReportPagination

    def get_queryset(self):
        return Report.objects.filter(owner=self.request.user).order_by("id")

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

旧コード表の注意:この旧ビュー表のファイル名もapi/url.pyですが、新例ではapi/views.pyです。serializerという単数のimport名も実ファイルと照合します。全Reportのquerysetに認可指定がないまま、モデル/import/URLだけを直して公開しません。router登録はアプリのurls.pyへまとめます。

api/url.py

import django_filters
from rest_framework import viewsets, filters, routers

from .models import Report
from .serializer import ReportSerializer

class TestView(viewsets.ModelViewSet):
    queryset = Report.objects.all()
    serializer_class = ReportSerializer

router = routers.DefaultRouter()
router.register(r'test', TestView)

 

動作の確認

開発サーバーを起動して「http://127.0.0.1:8000/api/test/」にアクセスする。

旧画面は序盤の「旧画面と新しい応答の違い」に保持しています。HTTP200や一覧が表示されることだけで、匿名拒否・他人の操作拒否・入力検証の成功とは判断しません。

上の旧コード/画面は保守資料です。新教材の確認範囲と結果は末尾に分けて示します。

新しいURLと結果の確認

GET/POSTは/api/test/、詳細GET/PUT/PATCH/DELETEは/api/test/{UUID}/です。新しいViewSetにclass属性querysetがないため、router.registerでbasename="report"を明示し、report-list/report-detailの逆引きも検査しています。一覧は一意なid順の既定20/最大50件で、UUID順は作成時刻順ではありません。同時更新中のsnapshotも保証しません。

新例では匿名は403、認証した所有者は自分の一覧/詳細を読めます。他人や存在しないUUIDは404にし、既知の他人のUUIDでも変更/削除できません。新規作成は201でid/message、削除は204で本文なしです。空の一覧もページング用のcount/next/previous/resultsを持つため、旧画像の配列だけの応答とは形が違います。

結果確認点
403このsession方式の匿名/CSRF拒否。CSRF成功だけで認可成功ではない。
400JSON構文/最上位object、messageの型/必須/長さ、許可外field。
404URL未接続・UUID形式・対象不在・所有者の範囲を分ける。
405/406methodとURL、Accept。新例はJSONRendererのみなので旧HTMLフォームは表示しない。
413/415解析する本文4096byteの上限、application/json/UTF-8。

SessionAuthenticationは認証済みsessionのunsafe methodでCSRFを検査します。トークン準備/送信はAjax POSTの教材を参照し、403の対処にCSRFを外したりCORS/許可Originを広げたりしません。このparserは使用するJSON本文の制限であり、未使用のDELETE本文等も含めたWeb層の容量/連打制限や監査を代行しません。通信失敗時に自動でPOSTを再送する機能もありません。

壊れた接続と認可不足を混同しない

旧表ではReportだけを定義しながらUser/Entryをimportし、Meta.modelもUserでした。またapi.urlsをincludeする一方でurl.pyという表記、アプリ側のurlpatterns不足、views/serializerのファイル名も食い違っていました。したがって「掲載どおり動く匿名CRUD」とは扱いません。

独立検体では未定義import、urlpatternsのないURLモジュール、querysetなしでbasenameを省略した登録がそれぞれ失敗することを確認しました。別の合成対照では、接続を成立させて全件queryset/既定AllowAnyとすると匿名操作/所有者の偽装が可能なことも確認しています。これは修正前の全コードが実行できた証明でも、CWiki本体にDRFの脆弱性があるという判定でもありません。応答の型はモデルJSON化と公開fieldも参照してください。

検証範囲と公式資料

2026年10月26日確認。Python3.12.10/Django5.2.18/DRF3.18.3/SQLite3.49.1の新しいメモリDBで13試験が成功しました。新例の5ファイルを実際にimportしてrouter/resolve/reverseを照合し、実User/DB sessionとCSRF検査で匿名/所有者/別人CRUD、UUID生成/入力偽装、拒否時Report不変、JSONの型/文字数/encoding/容量、一覧上限、404/405/406を検査しました。

本番ログイン/POST、DB schema/依存/設定の変更やmigrationは実行していません。実ブラウザ/Browsable API、TLS/proxy/CORS、Token/JWT、MySQL/PostgreSQL、並列更新/負荷、全業務認可・不正対策は未検証です。実データを削除するAPIとしてこの教材のDELETEを試さず、独立した合成データだけで確認します。

公式資料:router・basename・include、ViewSetとget_queryset、permissionの既定値、session認証とCSRF。

子ページ

子ページはありません

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

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