Django REST FrameworkでAPIキー認証する方法

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


settings.pyの編集


# 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の実装

次にざっくりと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/


APIキー認証の追加

現状ですが、認証なしで外部からのアクセスができる状態です。
試しに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

参考

Django REST Framework API Key

TOPページ