Django REST Frameworkで自作クラスに紐づけた API Key によるアクセス制限を行う


# この記事は何？
Django で Django REST Framework によるAPI開発を行う際、ユーザークラスではなく自作のクラス（デバイスなど）に紐づけたAPI Keyを発行し、APIで認証を行う方法のメモです。

# 環境
* django 4.2
* djangorestframework 3.14.0
* djangorestframework-api-key 2.3.0

# やりたいこと
Django WEBアプリ上で端末情報を登録し、**APIへのアクセスを登録された端末からのみに制限する**、ということをしたいです。

ここでは、**Django REST framework** を使用してAPIを実装し、**Django REST Framework API Key** を使用してAPIキーによるアクセス制限を行います。

* https://www.django-rest-framework.org/
* https://github.com/florimondmanca/djangorestframework-api-key

Django REST framework　にはトークン認証を行う [TokenAuthentication](https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication) が標準で含まれていますが、こちらは **DjangoのUserクラス** に紐づいてキーが発行されるようなので、今回は使用していません。

# 実装例
## model.py
端末を表す Device クラス、端末に紐づける DeviceAPIKey クラス、アクセス権限を与えるための HasDeviceAPIKey クラスを定義します。

```python
from django.db import models
from rest_framework_api_key.models import AbstractAPIKey
from rest_framework_api_key.permissions import BaseHasAPIKey

class Device(models.Model):
    """登録するデバイス情報"""

    id = models.AutoField("ID", primary_key=True)
    name = models.CharField("デバイス名", max_length=50, )
    created_at = models.DateTimeField("作成日時", auto_now_add=True)
    updated_at = models.DateTimeField("更新日時", auto_now=True)

    def __str__(self):
        return f"{self.name}"


class DeviceAPIKey(AbstractAPIKey):
    """デバイスに紐づけるAPIキークラス"""
    device = models.ForeignKey(
        Device,
        on_delete=models.CASCADE,
        related_name="api_keys",
    )


class HasDeviceAPIKey(BaseHasAPIKey):
    """認証用クラス"""
    model = DeviceAPIKey
```

## キー発行view例
デバイス登録時にAPIキーを発行し、画面に表示します。ユーザーは画面に表示されたキー情報を保存しておき、アクセス時に使用します。
```python
def device_create(request):
    if request.method == "POST":
        device_form = DeviceForm(request.POST)
        if device_form.is_valid():
            device = device_form.save(commit=False)
            # Deviceモデルに対してAPIキーを発行,紐づけ
            _, api_key = DeviceAPIKey.objects.create_key(name=device.id, device=device)
            context = {"api_key": api_key}
            return render(request, "Devices/device_create_complete.html", context=context) #デバイス登録完了画面で デバイスに対して発行した API Key を表示する.
    else:
        device_form = DeviceForm()

    return render(request, "Devices/device_create.html") #デバイス登録画面表示
```

## API View

**permission_classes に HasDeviceAPIKey を指定**することで、発行されたAPIキーがリクエストヘッダーに含まれていない場合、アクセスを拒否することができます。
また、ヘッダに含まれるキー情報から, キーに紐づくデバイスを取得することもできます。

```python
from devices.models import Device, DeviceAPIKey HasDeviceAPIKey
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView

class DeviceAPIView(APIView):
    permission_classes = [HasDeviceAPIKey] # DeviceAPIKey で発行したトークンがHeadersに含まれる場合のみアクセスを許可

    def get(self, request):
        # key に紐づくdeviceを取得
        api_key = request.META.get("HTTP_X_API_KEY")
        key = DeviceAPIKey.objects.get_from_key(api_key)
        device = Device.objects.get(api_keys=key)
        print(f"device {device.name} conntected.")
        return Response({"message": "Success"}, status=status.HTTP_200_OK)
```

## settings.py

以下を追記し、リクエスト時のヘッダー名を **X-Api-Key** に変更しています。

```python
#　header key名を X-Api-Keyに変更
API_KEY_CUSTOM_HEADER = "HTTP_X_API_KEY"
```

## リクエスト
登録後に表示された APIkey をリクエストヘッダーに含めて送信します。
APIKeyが不正な場合、アクセスが拒否され403レスポンスが返却されます。
```python
url = "api_url"
api_key="your key"
headers = {"X-Api-Key": api_key}
response = requests.get(url, headers=headers)
```

## その他
* 自身で検証はしていませんが、以下のソースを見ると、キー作成時に **expiry_date** を指定することで、**キーの有効期限**を指定することも可能そうです。
    * https://github.com/florimondmanca/djangorestframework-api-key/blob/master/src/rest_framework_api_key/models.py
* **通信時は HTTPS で暗号化**する必要があります。


# まとめ
ユーザー以外のモデルと紐づけてAPIのアクセス制限を行う方法を記載しました。
より高度な認証などが必要な場合は、この方法では不十分なので別の方法が必要です。