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.py
のINSTALLED_APPS
にoauth2_provider
を追加します。
INSTALLED_APPS = (
'django.contrib.admin',
...
'oauth2_provider',
...
)
さらに以下の設定をファイルの最後に追加します。
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
で認証の時にチェックするスコープを設定することができます。read
、write
を設定していますが、独自に追加することができます。
REST_FRAMEWORK
にはデフォルトで使用する認証と認可のクラスを設定します。
urls.py (Root)
次は、ルートの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
を編集して、Snippet
をUser
に紐付けます。ユーザーがある記事(スニペット)を投稿するようなイメージです。
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
から追記していきましょう。
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
です。前回にならってクラスベースのビューを追加しましょう。
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
UserList
とUserDetail
クラスを追加しました。
それとすべてのクラスにpermission_classes
という属性を追加しました。
ここで使用したpermissions
のクラスを軽く説明すると、
-
permissions.AllowAny
はその名の通り、トークンを持っていなくても誰でも認証が通ります。 -
permissions.IsAuthenticatedOrReadOnly
はGET
メソッドに対しては誰でも認可します。POST
、PUT
、PATCH
、DELETE
などの安全でないメソッドに対しては認証を要求します。
Permissionのクラスについては他にもたくさんあるので、こちらの公式ドキュメントをご参照ください。
urls.py (Project)
次は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/
にアクセスしてみてください。
こんな画面が出たかと思います。(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_classes
にpermissions.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
となるように登録したいです。
ここら辺の話はまた追って別記事で書きたいと思います。
ここまでありがとうございました。