スポンサーリンク

【その5】チュートリアルから学ぶDjangoRESTframework

【その5】チュートリアルから学ぶDjangoRESTframework

前回はクラスビューベースで処理を記述していく方法について「DjangoRESTframework」の使い方を学んでいきました。
今回は、公式ドキュメントのチュートリアルに沿って「Tutorial 4: Authentication & Permissions」について解説していきたいと思います。
チュートリアルを簡単に日本語訳したものになりますので、英語に抵抗がない方は公式ドキュメントを参考にすることをお勧めします。
本記事の目的は「DjangoRESTframework」の入門編として読み物的な感じで読んでいただけますと幸いです。

ユーザー認証機能を実装する

今までは、誰でもスニペットを取得・作成・変更・削除ができましたが、実際にAPIを実装しようとするときはユーザー認証が必要ですよね。
なので今回は、ユーザー認証機能を実装する方法を解説していきます。

Modelに情報を追加する

ユーザー認証機能を実装するには、スニペットを誰が作成したのかの情報を追加していく必要がありますので追加していきましょう。
※ユーザーモデルはDjangoにデフォルトで実装されているユーザーモデルを使用します

snippeets/models.pyファイル内のSnippetsモデルの中に下記コードを追加していきます。

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

また、スニペットを保存した時に強調されたHTMLを作成したいので「pygments」というライブラリを使って、保存時に強調されるようにしたいと思います。
こちらもsnippeets/models.pyファイル内のSnippetsモデルの中に下記コードを追加していきます。

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    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(Snippet, self).save(*args, **kwargs)

実際にすべて書き換えたmodels.pyはこんな感じになります。

from django.db import models
from pygments import highlight
from pygments.formatters.html import HtmlFormatter
from pygments.lexers import get_all_lexers, get_lexer_by_name
from pygments.styles import get_all_styles

LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    linenos = models.BooleanField(default=False)
    language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
    style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
    owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
    highlighted = models.TextField()

    def save(self, *args, **kwargs):
        """
        「pygments」ライブラリを使ってHTMLを使って強調表示された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(Snippet, self).save(*args, **kwargs)

    class Meta:
        ordering = ['created']

情報を追加したのでmaigrateしていきたいのですが、このままではmakemigrationsが通りません。
(すでにレコードが存在するので、そのレコードに対して今回追加したカラムをどう扱うか決まっていないため)
通常は、DBを移行したり追加したカラムにデフォルト値を設定する、もしくはNullを許容させるのですが、今回はDBをまるっと削除してしまいましょう。
また、makemigrationsし直したいので、今までのmaigrationsも削除しておきます。

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

DBを削除したので管理者ユーザーを作り直しておきます。
ここでは仮に、IDをadminPWをpassword123しておきます。

python manage.py createsuperuser

これで下準備はできましたので、実際に認証機能を実装していきましょう。

ユーザーモデルのエンドポイントを追加する

ユーザーモデルについてもAPIで操作できるようにエンドポイントを追加していきます。
まずは、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']

次に、snippets/views.pyにもクラスベースのビューを追加していきます。

from snippets.serializers import UserSerializer
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

最後にsnippets/urls.pyにエンドポイントを追加します。

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

これでエンドポイントを追加することができました。

スニペットとユーザーの紐づけをする

.perform_create()関数をオーバーライドしてスニペットを保存する時に誰が作成したのかの情報を保存していきます。
snippets/views.py内にあるSnippetListにオーバーライドしたものを追加していきます。

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

シリアライザーの更新

スニペットを作成したユーザーを紐づけることができたので、シリアライザーも更新しておきます。
snippets/serializers.py内のSnippetSerializerに下記コードを追加していきます。

owner = serializers.ReadOnlyField(source='owner.username')

また、Metaクラス内のfieldsにもownerを追加してください。

最終的にはこんな感じになります。

from django.contrib.auth.models import User
from rest_framework import serializers

from snippets.models import Snippet


class SnippetSerializer(serializers.ModelSerializer):
    owner = serializers.ReadOnlyField(source='owner.username')

    class Meta:
        model = Snippet
        fields = ['id', 'owner', 'title', 'code', 'linenos', 'language', 'style']


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

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

Viewsに認証機能を実装する

認証されたユーザーのみ作成、更新、削除できるようにしていきます。
REST frameworkが用意しているIsAuthenticatedOrReadOnlyを利用して実装していきます。

snippets/views.py内でモジュールをインポートします。

from rest_framework import permissions

モジュールをインポートできましたら、SnippetListクラスとSnippetDetailクラスに下記コードを追加します。

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

ブラウザからログインできるようにする

現状、ブラウザからではログインできないのでスニペットを作成することができなくなっています。
なので、ブラウザからもログインできるようにしておきましょう。
tutorial/urls.pyファイルに以下を追加していきます。

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

最終的にはこんな感じになります。

from django.urls import path, include

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

オブジェクトの認証機能の実装

まず、認証権限について記述していきます。
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

GETやHEAD、OPTIONSメソッド等は読み込みだけですので、それらのメソッドでリクエストされた場合はTrueを返します。
それ以外は、オブジェクトのオーナーとリクエストしたユーザーが同じであればTrueを返します。

先程snippets/views.py内のSnippetDetailクラスに追加した箇所を変更します。

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

また、IsOwnerOrReadOnlyモジュールをインポートしなければ動きませんので、インポートしておきましょう。

from snippets.permissions import IsOwnerOrReadOnly

snippets/views.pyは最終的にはこんな感じになります。

from django.contrib.auth.models import User
from django.http import Http404
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView

from snippets.models import Snippet
from snippets.permissions import IsOwnerOrReadOnly
from snippets.serializers import SnippetSerializer, UserSerializer


class SnippetList(generics.ListCreateAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]

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


class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer
    permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                          IsOwnerOrReadOnly]


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


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

これで、ユーザー認証機能を実装できました。

APIを実際に叩いてみる

これで実装できましたので、最後に実際にAPIを叩いてみましょう。
認証方法はデフォルトのSessionAuthenticationBasicAuthenticationになっておりますので、ブラウザからログインした場合はセッションから認証して、APIからリクエストする場合はBasic認証を利用してリクエストを出します。

まずは、認証情報なしでリクエストしてみます。

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

中略
{
    "detail": "認証情報が含まれていません。"
}

認証情報が無いのでスニペットを作成することができませんでしたね。
それでは、次に認証情報を追加してリクエストしてみたいと思います。

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

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

-a admin:password123の部分は、createsuperuserコマンドで管理者ユーザーを作成した際のIDとPWを指定します。
しっかり認証機能が実装できましたね。
今回はここまでにしたいと思います。

まとめ

今回はユーザー認証を実装する方法を解説していきました。
次回はハイパーリンクの関係性をAPIで実装する方法を解説していきます。
不明点等ございましたら、コメントかTwitterのDMでご質問ください。

コメント