Django REST FrameworkとNuxt3を使ってサイトを作っているのですが、バックエンド部分はなるべくセキュアな状態を保ちたいです。
ネットで調べていたらAPIキー認証を使えるライブラリがあるみたいなので、そちらを使ってみることにしました。
つまづいたポイントもあったので、備忘をかねてAPIキー発行、認証までの手順をまとめていきます。
Django==3.2.13
djangorestframework==3.13.1
djangorestframework-api-key==2.2.0
簡単なブログサイトを例にします。
django-admin startproject project .
python manage.py startapp blog
# project/settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog.apps.BlogConfig', # 追加
'rest_framework', # 追加
"rest_framework_api_key", # 追加
]
通常は他にもいろいろいじるのですが、テストサイトなので、とりあえず
タイトルとテキストだけのシンプルな構成です。
# blog/models.py
from django.db import models
class Post(models.Model):
"""ブログ"""
title = models.CharField('タイトル', max_length=40)
text = models.TextField('本文')
管理画面にも加えておきましょう。
# blog/admin.py
from django.contrib import admin
from .models import Post
admin.site.register(Post)
次にmodelをmigrateします。
python manage.py makemigrations
>>>
Migrations for 'blog':
blog\migrations\0001_initial.py
- Create model Post
python manage.py migrate
>>>
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, rest_framework_api_key, sessions
Running migrations:
# ...省略
Applying blog.0001_initial... OK
Applying rest_framework_api_key.0001_initial... OK
Applying rest_framework_api_key.0002_auto_20190529_2243... OK
Applying rest_framework_api_key.0003_auto_20190623_1952... OK
Applying rest_framework_api_key.0004_prefix_hashed_key... OK
Applying rest_framework_api_key.0005_auto_20220110_1102... OK
Applying sessions.0001_initial... OK
そうすると、API KEY保管用のモデルが作られたことが分かります。
super userを作って管理画面にアクセスしてみましょう。
API Keysというモデルが作られています。
とりあえず今はなにもせず適当なPostを加えるだけにします。
次にざっくりとREST APIを実装していきます。
まずはプロジェクトのurls.pyにapiルートを作ります。
# project/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import url, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/blog/', include('blog.urls')),
]
次にブログアプリにurls.pyを作成します。
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('posts/', views.PostList.as_view(), name='post_list'),
path('posts/<int:pk>/', views.PostDetail.as_view(), name='post_detail'),
]
同じくブログアプリ内に、serializers.pyを作成します。
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = Post
fields = '__all__'
最後にビューです。
# blog/views.py
from rest_framework import generics
from .models import Post
from .serializers import PostSerializer
class PostList(generics.ListAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
この段階でrunserverしてAPIを確認します。
http://127.0.0.1:8000/api/blog/posts/
こちらは詳細ビューですね。
http://127.0.0.1:8000/api/blog/posts/1/
現状ですが、認証なしで外部からのアクセスができる状態です。
試しにDjangoサーバーを起動させたままで、適当なrequestを送ってみましょう。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
r = requests.get(url)
print(r.status_code)
print(r.text)
>>>
200
[{"id":1,"title":"今日はゴルフの練習に行った","text":"200球くらい打ったのでとても疲れた。"},{"id":2,"title":"今日はブログを書いた","text":"ブログを作るのはとても楽しい"}]
このようにレスポンスが取得できています。
キー認証を付与する方法ですが、views.pyにライブラリのHasAPIKey
パーミッションを加えます。
# blog/views.py
# ...省略
from rest_framework_api_key.permissions import HasAPIKey # 追加
class PostList(generics.ListAPIView):
permission_classes = [HasAPIKey] # 追加
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveAPIView):
permission_classes = [HasAPIKey] # 追加
queryset = Post.objects.all()
serializer_class = PostSerializer
そうすると、以下のようにこのアクションを実行する権限がありません。
となります。
これだと不便なので、ログイン中は表示させる場合はrest frameworkのIsAuthenticated
パーミッションを付与します。
# blog/views.py
# ...省略
from rest_framework_api_key.permissions import HasAPIKey
from rest_framework.permissions import IsAuthenticated # 追加
class PostList(generics.ListAPIView):
permission_classes = [HasAPIKey | IsAuthenticated] # 変更
queryset = Post.objects.all()
serializer_class = PostSerializer
class PostDetail(generics.RetrieveAPIView):
permission_classes = [HasAPIKey | IsAuthenticated] # 変更
queryset = Post.objects.all()
serializer_class = PostSerializer
そうすると表示されるようになります。
この状態で再びリクエストを送ってみましょう。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
r = requests.get(url)
print(r.status_code)
print(r.text)
>>>
403
{"detail":"認証情報が含まれていません。"}
外部からのリクエストには意図していた通りに403エラーが返りました。
認証を付ける方法は分かったので、次にキー情報をリクエストに付与していきます。
管理画面からAPIキーを作成すると、上の方にアラートメッセージでAPIキーが表示されます。
もしくは対話的なコマンドで作ることも可能です。
python manage.py shell
>>> from rest_framework_api_key.models import APIKey
>>> api_key, key = APIKey.objects.create_key(name="qlitre-api-key")
>>> key
'jyO1g5wE.VEMhNfBx4SiyVJEpyPHP7u5VG5yHYh8P'
確認したら抜けましょう。
>>> exit()
管理画面に戻ると、正しく追加されていることが分かります。
次にこのキーを付与してリクエストを送ってみましょう。
以下のようにします。
import requests
url = 'http://127.0.0.1:8000/api/blog/posts/'
key = 'jyO1g5wE.VEMhNfBx4SiyVJEpyPHP7u5VG5yHYh8P'
headers = {"Authorization": f"Api-Key {key}"}
r = requests.get(url, headers=headers)
print(r.status_code)
print(r.text)
>>>
200
[{"id":1,"title":"今日はゴルフの練習に行った","text":"200球くらい打ったのでとても疲れた。"},{"id":2,"title":"今日はブログを書いた","text":"ブログを作るのはとても楽しい"}]
Api-Key {key}
という風に半角スペースをあけるのがポイントです。
ただちょっと直感的じゃないですね。
settings.pyに以下のように追加すると、やりやすいです。
# project/settings.py
# ...省略
API_KEY_CUSTOM_HEADER = "HTTP_X_API_KEY"
そうすると、こういう風に認証を付与できます。
key = 'jyO1g5wE.VEMhNfBx4SiyVJEpyPHP7u5VG5yHYh8P'
headers = {"X-Api-Key" : key}
r = requests.get(url, headers=headers)
分かりやすくなりました。
フロントからリクエストを送るときも基本は同じです。
例えばNuxt.jsからfetchする場合。
const headers = { 'X-Api-Key': apiKey }
const data = await $fetch(`http://127.0.0.1:8000/api/blog/posts/`, { params: params, headers: headers })
https://github.com/qlitre/djangorestframework-api-key-test