0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django REST framework(DRF)公式チュートリアル④ Authentication & Permissions

Last updated at Posted at 2025-05-28

Tutorial 4: Authentication & Permissions

はじめに

本記事は、Django REST framework の公式チュートリアルTutorial 4: Authentication & Permissionsの内容をもとに、和訳および補足解説を行ったものです。

基本的にはチュートリアルの翻訳を軸に構成しており、✏️補足の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。

他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇
DRF(Django REST framework)公式チュートリアルを日本語でまとめてみた【全6回】

それでは、本チュートリアルを始めましょう。


現状、このAPIには、誰がコードスニペットを編集または削除できるかについての制限がありません。
以下のことを確実にするため、もう少し高度な動作を実装したいと考えています:

・コードスニペットは常に作成者と紐づいている。
・スニペットは認証済みのユーザーのみ作成できる。
・スニペットの作成者だけが、それを更新・削除できる。
・認証されていないリクエストには、読み取り専用のアクセス権が与えられる。

Modelへの情報の追加

Snippet モデルクラスにいくつか変更を加えます。
まずは、いくつかのフィールドを追加してみましょう。
そのうちの1つは、コードスニペットを作成したユーザーを表すためのフィールドです。
もう1つは、ハイライトされたHTML形式のコードを保存するためのフィールドです。

models.py にある Snippet モデルに、以下の2つのフィールドを追加してください。

snippets/models.py
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()

Modelを保存する際には、pygments というコードのハイライトライブラリを使って、highlighted フィールドに値を設定するようにする必要もあります。

そのために、いくつか追加のインポートが必要になります:

snippets/models.py
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

そして、これでModelクラスに .save() メソッドを追加します。

snippets/models.py
def save(self, *args, **kwargs):
    """
    pygments ライブラリを使用して、コードスニペットのハイライトされた HTML 表現を作成します。
    """
    lexer = get_lexer_by_name(self.language)
    linenos = 'table' if self.linenos else False
    options = {'title': self.title} if self.title else {}
    formatter = HtmlFormatter(style=self.style, linenos=linenos,
                              full=True, **options)
    self.highlighted = highlight(self.code, lexer, formatter)
    super().save(*args, **kwargs)

全ての作業が完了したら、データベースのテーブルを更新する必要があります。
通常であればそのためにマイグレーションを作成しますが、このチュートリアルでは、データベースを削除して最初からやり直すことにしましょう。

rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate

APIのテスト用に、いくつかのユーザーを作成しておくとよいでしょう。
最も手早い方法は、createsuperuser コマンドを使うことです。

python manage.py createsuperuser

✏️補足

Modelクラスの save() メソッドは、オブジェクトをデータベースに保存する際に呼び出されます。
このメソッドをオーバーライドすることで、保存処理の前後に任意の処理を追加できます。
以下では、highlighted フィールドにハイライト済みの HTML を生成して保存するために、save() メソッドをオーバーライドしています。
※各コード行の説明は、コメントとして記述しています(ハイライト済みの HTML を生成する処理の解説です)

snippets/models.py
# ModelオブジェクトをDBに保存する処理を行うメソッド。
# 保存処理をカスタマイズしたいときにはオーバーライドをおこなう。
def save(self, *args, **kwargs):
    lexer = get_lexer_by_name(self.language)  # 使用するプログラミング言語の構文定義を取得
    linenos = 'table' if self.linenos else False  # 行番号をつけるかどうか
    options = {'title': self.title} if self.title else {}  # タイトルがある場合はオプションに含める

    formatter = HtmlFormatter(
        style=self.style,     # 色合い(テーマ)の指定
        linenos=linenos,      # 行番号の表示方法
        full=True,            # 全体を含む HTML ドキュメントとして出力する
        **options             # タイトルがあれば、それを含める
    )

    # self.code を指定した言語(lexer)とフォーマット(HTML)でハイライトし、self.highlighted に保存する
    self.highlighted = highlight(self.code, lexer, formatter)

    # 親クラスの save() を呼び出して、通常の保存処理を実行
    super().save(*args, **kwargs)


Userモデルにエンドポイントを追加する

これで操作できるユーザーが用意できたので、これらのユーザーを API上で確認できるようにしておくとよいでしょう。
新しい Serializer の作成は簡単です。serializers.py に以下を追加しましょう:

snippets/serializers.py
from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

snippetsUser モデルにおけるリバースリレーションであるため、ModelSerializer クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。

views.py にいくつかの View も追加しましょう。
ユーザーの表現(ユーザー情報のAPI)には読み取り専用の View だけを使いたいので、ListAPIViewRetrieveAPIView というジェネリッククラスベースビューを使用します。

snippets/views.py
from django.contrib.auth.models import User


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

UserSerializer クラスも忘れずにインポートしてください。

snippets/views.py
from snippets.serializers import UserSerializer

最後に、それらのビューをAPIに追加する必要があります。URL設定からそれらを参照することで実現できます。次のコードを snippets/urls.pyurlpatterns に追加してください。

snippets/urls.py
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

✏️補足

snippets は User モデルにおけるリバースリレーションであるため、ModelSerializer クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。

これはどういうことかというと、
Userモデル と Snippetモデルは、Snippetモデル側の次の定義によって関連付けられています:

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)

しかし、Userモデル自身には Snippet との関係を示すフィールドを直接定義していないため、ModelSerializer クラスを使っただけでは自動的に snippets フィールドは含まれません。

# Model定義の一部抜粋
class Snippet(models.Model):
    ...
    # Snippetとのリレーションを明示的に定義している
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)

class User(AbstractUser):
    # Snippetとのリレーションを明示的に定義しているわけではない
    username = models.CharField(...)
    first_name = models.CharField(max_length=150, blank=True)
    ...

このように、Snippet側からのリバースリレーションによって user.snippets のようなアクセスは可能になりますが、
それを Serializer上で表現するには明示的にフィールドとして定義する必要がある というわけです。

そのため、以下のように PrimaryKeyRelatedField を使って、User に紐づくすべての Snippet を取得・表示できるようにします:

snippets/serializers.py
# Serializerクラス
class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

また、PrimaryKeyRelatedField() メソッドの仕様は、
第1引数の many=True によって「複数の Snippet をリストとして取得する」ことを示し、
第2引数の queryset に関連付ける Snippet モデルの全オブジェクトを指定します。

この設定により、そのユーザーと紐づく Snippet オブジェクトだけがフィールドとして取得・表示されるようになります。

                                        # リスト指定で複数のSnippetを取得   # Snippet全体から、ユーザーに紐づくものが対象となる
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())


SnippetをUserと関連付ける

現時点では、コードスニペットを作成しても、それを作成したユーザーとスニペットインスタンスを関連付ける方法がありません。
ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。

この問題に対処するため、スニペットビューで .perform_create() メソッドをオーバーライドします。このメソッドを使うことで、インスタンスの保存方法をカスタマイズでき、リクエストやURLに暗黙的に含まれる情報を処理することができます。

SnippetList ビュークラスに、次のメソッドを追加してください:

snippets/views.py
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)

Serializer の create() メソッドには、リクエストからの検証済みデータ( validated data )に加えて、追加の owner フィールドも渡されるようになります。

Serializerを更新する

Snippetがそれを作成したユーザーと関連付けられるようになったので、それを反映するために SnippetSerializer を更新しましょう。
serializers.py のSerializer定義に、次のフィールドを追加してください:

snippets/serializers.py
owner = serializers.ReadOnlyField(source='owner.username')

注意Meta クラスの fields リストに owner を追加するのも忘れないでください。

このフィールドは、なかなか興味深いことをしています。
source 引数は、どの属性を使ってフィールドの値を埋めるかを指定するもので、シリアライズされるインスタンスの任意の属性を参照できます。
また、上記のように「ドット記法(owner.username のような形式)」を使うこともでき、その場合は Django のテンプレート言語と同様に、指定された属性を順にたどって値を取得します。

今回追加したフィールドは、CharFieldBooleanField のような型付きフィールドとは異なり、型指定のない ReadOnlyField クラスです。
ReadOnlyField は常に読み取り専用であり、シリアライズ時には使われますが、デシリアライズ時(リクエストからModelを更新するとき)には使われません。

この用途には、CharField(read_only=True) を使うこともできます。

✏️補足

ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。

これはどういうことかというと、例えば Snippet 作成用の POST リクエストがクライアントから送信されたとしても、下記のようにリクエストボディにはユーザー情報(例:"user": 1 )が含まれません。そのため、そのままでは作成される Snippet インスタンスがどのユーザーに紐づいているのか判断できないということです。

POST /snippets/
{
    "title": "Hello World",
    "code": "print('Hello, world!')"
}

しかし、ログインしているユーザー情報は認証情報( request.user )としてセットされてサーバー側に送られるため、以下のように、リクエストからの検証済みデータ(validated_data)に加えて request.user も渡しています。

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

また、perform_create() は、serializer.save() メソッドを呼び出すためのフックメソッドで、通常ではデフォルトの動作として自動で実行されます。
ただし、検証済みデータ(validated_data)以外にも情報を渡したい場合、任意の値を追加する処理を加えてオーバーライドします。
例えば以下のように、第1引数にフィールド名( owner )を指定し、その値として self.request.user (ログイン中のユーザー情報)を渡すことで、作成される Snippetインスタンスにユーザー情報を紐づけることができます。

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

Viewに必要なパーミッションを追加する

コードスニペットがユーザーと関連付けられたので、認証されたユーザーだけがコードスニペットを作成・更新・削除できるようにしたいです。

Django REST framework には、特定の View へのアクセスを制限するための パーミッションクラス がいくつか用意されています。
今回使いたいのは IsAuthenticatedOrReadOnly です。
これを使うことで、認証されたリクエストは読み書きのアクセスを持ち、認証されていないリクエストは読み取り専用アクセスのみになります。

まずは、views.py モジュールに以下のインポートを追加してください:

snippets/views.py
from rest_framework import permissions

そして、以下のプロパティを SnippetListSnippetDetail の両方の Viewクラスに追加してください。

snippets/views.py
permission_classes = [permissions.IsAuthenticatedOrReadOnly]

✏️補足

permisson_classes は、Viewクラスに定義するアクセス制御用のクラス変数です。
この設定でアクセスを拒否する仕組みは以下になります。

  1. リクエストがくる
     例)ユーザーが POST /snippets/ を送信します。
  2. Viewに設定された permission_classes をチェック
     例)permission_classes = [xxx] のように設定されたクラスのメソッドが呼び出されます。
  3. 許可判定を行う
     メソッド内で request.method などを確認し、許可するかどうかを(True / False)で判断します。
  4. 許可されなければ 403 Forbidden を返す
     False が返ると、Viewの処理には進まず、自動的にアクセスが拒否されます。

ブラウズ可能なAPIにログイン機能を追加する

現在、ブラウザでブラウズ可能なAPIにアクセスしても、新しいコードスニペットを作成できません。これを行うには、ユーザーとしてログインできる必要があります。

ブラウズ可能なAPIで使用するためのログインビューは、プロジェクトレベルの urls.py ファイルを編集することで追加できます。

まず、ファイルの先頭に以下のインポートを追加してください:

snippets/urls.py
from django.urls import path, include

そして、ファイルの末尾に、ログインおよびログアウトビューを含めるためのパターンを追加します:

snippets/urls.py
urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]

api-auth/ の部分は、実際には任意のURLに変更できます。

ブラウザを再度開いてページを更新すると、画面右上に「Log in」リンクが表示されます。先ほど作成したユーザーのいずれかでログインすれば、再びコードスニペットを作成できるようになります。

コードスニペットをいくつか作成したら、/users/ エンドポイントにアクセスしてみてください。すると、それぞれのユーザーの snippets フィールドに、そのユーザーに関連付けられたスニペットIDのリストが表示されていることが分かるはずです。

✏️補足

以下のコードで、ブラウザ可能なAPIにログインビューを追加できる理由としては、rest_framework.urls にログイン・ログアウト用のViewとテンプレートが含まれているから です。

snippets/urls.py
urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]

これを include() することで、次のようなURLが使えて、ログイン・ログアウトビューを実装することができます:

  • /api-auth/login/ でログイン画面が表示される
    259908DB-3C52-4E32-84CA-9A8F6899AFE3_4_5005_c.jpeg

  • /api-auth/logout/ でログアウトできる
    80113807-5625-40E0-BBF1-7349847449E0.jpeg

ちなみに、rest_framework.urls を追加した場合にはトップページの赤枠の箇所にログイン画面へのリンクが生成されます。

2A6ABE7F-A2FC-45FA-9D05-B6A6167375D6.jpeg


オブジェクトレベルのパーミッション

全てのコードスニペットを誰でも閲覧できるようにしたいですが、同時にその Snippet を作成したユーザーだけが更新や削除できるようにしたいです。

そのためには、カスタムパーミッションを作成する必要があります。

snippets アプリ内に、新しいファイル permissions.py を作成してください。

snippets/permissions.py
from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    オブジェクトの所有者のみが編集できるようにするカスタムパーミッション。
    """

    def has_object_permission(self, request, view, obj):
        # 読み取りパーミッションはすべてのリクエストに許可されます。
        # したがって、GET、HEAD、OPTIONS リクエストは常に許可されます。
        if request.method in permissions.SAFE_METHODS:
            return True

        # 書き込みパーミッションはスニペットの所有者にのみ許可されます。
        return obj.owner == request.user

これで、SnippetDetail Viewクラスの permission_classes プロパティを編集することで、作成したカスタムパーミッションをスニペットインスタンスのエンドポイントに追加できるようになりました:

snippets/views.py
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

また、IsOwnerOrReadOnly クラスをインポートするのを忘れずに行ってください:

snippets/permissions.py
from snippets.permissions import IsOwnerOrReadOnly

これで、ブラウザを再度開いて Snippet の個別エンドポイントにアクセスすると、DELETE や PUT の操作は、その Snippet を作成したユーザーとしてログインしている場合にのみ表示されるようになります。

APIでの認証

現在、APIにパーミッションが設定されているため、Snippet を編集したい場合はリクエストに認証が必要になります。
まだ認証クラスを設定していないため、現在はデフォルトの設定が適用されており、SessionAuthenticationBasicAuthentication が使用されています。

ウェブブラウザを通じてAPIとやり取りする場合は、ログインすることでブラウザのセッションがリクエストに必要な認証情報を提供してくれます。

一方で、プログラムなどからAPIとやり取りする場合は、各リクエストに対して明示的に認証情報を付与する必要があります。

認証せずに Snippet を作成しようとすると、エラーが発生します:

http POST http://127.0.0.1:8000/snippets/ code="print(123)"

{
    "detail": "Authentication credentials were not provided."
}

先ほど作成したユーザーのユーザー名とパスワードを含めることで、リクエストを成功させることができます。

http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"

{
    "id": 1,
    "owner": "admin",
    "title": "foo",
    "code": "print(789)",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

まとめ

これで、Web API に対してかなり細かく制御されたパーミッションを設定できるようになり、システムのユーザーや、彼らが作成したコードスニペット用のエンドポイントも整いました。

チュートリアルのパート5では、ハイライト表示された Snippet用の HTML エンドポイントを作成することで全体をまとめ、システム内の関係性にハイパーリンクを使用することで API の一貫性を高める方法について学びます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?