12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Django REST frameworkチュートリアル その4

Last updated at Posted at 2019-05-25

Tutorial 4: Authentication & Permissions

前回の記事「Django REST frameworkチュートリアル その3」の続きです。

今回は認証処理を作っていきます。公式チュートリアルの実装とは若干違ったトークン認証を実装していきたいと思います。

django-oauth-toolkitのインストール

django-oauth-toolkitを使いますので、django-oauth-toolkit公式ドキュメントも適宜ご参照ください。

まずpipで仮想環境にdjango-oauth-toolkitをインストールしましょう。

pip install django-oauth-toolkit

settings.py

settings.pyINSTALLED_APPSoauth2_providerを追加します。

tutorial/settings.py
INSTALLED_APPS = (
    'django.contrib.admin',
    ...
    'oauth2_provider',
    ...
)

さらに以下の設定をファイルの最後に追加します。

tutorial/settings.py
OAUTH2_PROVIDER = {
    # this is the list of available scopes
    'SCOPES': {'read': 'Read scope', 'write': 'Write scope'}
}

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
    ),    
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

OAUTH2_PROVIDER.SCOPESで認証の時にチェックするスコープを設定することができます。readwriteを設定していますが、独自に追加することができます。
REST_FRAMEWORKにはデフォルトで使用する認証と認可のクラスを設定します。

urls.py (Root)

次は、ルートのurls.pyを編集して、認証のためのエンドポイントを作成しましょう。

tutorial/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('oauth2/', include('oauth2_provider.urls', namespace='oauth2_provider')),
    path('', include('snippets.urls')),
]

oauth2/のエンドポイントを追加しました。このエンドポイントから、後ほど認証のアプリ登録やトークン発行などを行います。

models.py

次に、models.pyを編集して、SnippetUserに紐付けます。ユーザーがある記事(スニペット)を投稿するようなイメージです。

snippets/models.py
from django.db import models


class Snippet(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    title = models.CharField(max_length=100, blank=True, default='')
    code = models.TextField()
    owner = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    class Meta:
        ordering = ('created',)

外部キーのownerフィールドを追加しました。

モデルを変更したので、一旦前回までのDBの中身を消して、再度マイグレーションしましょう。

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

さらにテスト用にsuperuserを作っておいて、admin/から何人か適当なユーザーを作っておきます。

python manage.py createsuperuser
# ユーザー名の入力
# パスワードの入力
# パスワード再確認
python manage.py runserver
# http://localhost:8000/にアクセスして、上で入力したユーザー名・パスワードでログイン.
# 適当にユーザーを作成する.

ここまで大丈夫でしょうか。編集するファイルが多くてDjangoは大変ですね。休憩を入れつつゆっくり進めていきましょう。

serializers.py

次は、APIの方からもユーザーを作成したり、削除したりできるようにしたいので、その機能を作っていきます。
まずseirializers.pyから追記していきましょう。

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


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

    class Meta:
        model = Snippet
        fields = ('id', 'title', 'code', 'owner')


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('id', 'username', 'email')

UserSerializerクラスを追加しました。

views.py

そうしましたら、次はviews.pyです。前回にならってクラスベースのビューを追加しましょう。

snippets/views.py
from rest_framework import generics
from rest_framework.response import Response
from snippets.models import Snippet
from django.contrib.auth.models import User
from snippets.serializers import SnippetSerializer, UserSerializer


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


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


class UserList(generics.ListCreateAPIView):
    permission_classes = [permissions.AllowAny]
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetails(generics.RetrieveUpdateDestroyAPIView):
    permission_classes = [permissions.IsAuthenticatedOrReadOnly]
    queryset = User.objects.all()
    serializer_class = UserSerializer

UserListUserDetailクラスを追加しました。
それとすべてのクラスにpermission_classesという属性を追加しました。
ここで使用したpermissionsのクラスを軽く説明すると、

  • permissions.AllowAnyはその名の通り、トークンを持っていなくても誰でも認証が通ります。
  • permissions.IsAuthenticatedOrReadOnlyGETメソッドに対しては誰でも認可します。POSTPUTPATCHDELETEなどの安全でないメソッドに対しては認証を要求します。

Permissionのクラスについては他にもたくさんあるので、こちらの公式ドキュメントをご参照ください。

urls.py (Project)

次はurls.pyです。ビューをエンドポイントに紐付けます。

snippets/urls.py
from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
    path('users/', views.UserList.as_view()),
    path('users/<int:pk>/', views.UserDetails.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

users/users/<pk>/のエンドポイントを追加です。

完成

これで認証処理が一通り完成しました。
期待する動作としては、以下です。

  • ユーザーの新規作成・一覧取得は誰でもアクセス可能。
  • 特定ユーザー情報の取得は誰でもアクセス可能だが、変更・削除については認証が必要。
  • スニペットの一覧取得は誰でも可能だが、新規作成には認証が必要。
  • 特定スニペット情報の取得は誰でもアクセス可能だが、変更・削除については認証が必要。

テスト

認証するアプリの登録

次は、簡単なテストをしてみましょう。
まずローカルサーバーを起動します。

python manage.py runserver

一回http://localhost:8000/admin/から先ほど作った管理者権限のアカウントでログインしてください。(これをしないと次のアプリ登録の画面に行けなかったです。)

次にhttp://localhost:8000/oauth2/applications/にアクセスしてみてください。

スクリーンショット 2019-05-25 17.39.03.png

こんな画面が出たかと思います。(hogeは私が登録したアプリなので、最初にアクセスした時はhogeはないです。)
New Applicationをクリックして認証トークンを発行するアプリを登録します。(ここではこれまで作ってきたAPIアプリを登録します。)
それぞれの項目は以下のように設定します。

  • Name: 適当な名前
  • Client Type: confidential
  • Authorization Grant Type: Resource owner password-based

すると、Client idとClient secretが取得できます。これらは、この後使います。

トークン発行

これでトークンを発行する準備が整いました。
早速トークンを発行してみましょう。以下のリクエストを送ります。

curl -X POST -d "grant_type=password&username=<user_name>&password=<password>" -u"<client_id>:<client_secret>" http://localhost:8000/oauth2/token/

<username>のところに登録したユーザー名を、<password>のところにそのユーザーのパスワードを入れます。<client_id><client_secret>には先ほど取得したものを入れます。

{
    "access_token": "<your_access_token>",
    "token_type": "Bearer",
    "expires_in": 36000,
    "refresh_token": "<your_refresh_token>",
    "scope": "read write"
}

こんなレスポンスが返ってきましたでしょうか。
いい感じですね。このaccess tokenを使ってユーザーを認証します。(その他のパラメータについてはここでは説明しません。ごめんなさい。。)

リクエスト

今度はヘッダーにaccess tokenをつけてリクエストを送ってみましょう。

curl -H "Authorization: Bearer <your_access_token>" http://localhost:8000/users/

ユーザー一覧のレスポンスが返ってきたでしょうか。
ただこのエンドポイントはpermission_classespermissions.AllowAnyを指定しましたので、ヘッダーにaccess tokenをつけなくても同じ結果が返ってきます。

こっちのリクエストはどうでしょうか。

curl -X PATCH -H 'Content-Type:application/json' -H "Authorization: Bearer <your_access_token>" -d '{"code":"This is modified."}' http://localhost:8000/snippets/1/

こちらはaccess tokenなしだとはじかれてしまいますね。
認証処理が正しく動作しているのが確認できました。
余裕があれば他のエンドポイントの他のメソッドについても同様に試してみてください。

TODO

ここまでで認証処理が実装できました。
が、認証周りでもう少し改善しなければいけないところは次のようなところです。

  • トークンを持っていれば他の人のユーザー情報やスニペットの中身を書き換えることができてしまいます。変更や削除などのリクエストを送った人がそのデータのオーナーなのかをチェックしたいです。
  • スニペットを登録するときに自動でリクエストを送った人がownerとなるように登録したいです。

ここら辺の話はまた追って別記事で書きたいと思います。
ここまでありがとうございました。

12
11
1

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
12
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?