KIMUSON.DEV

Django REST frameworkでトークン認証をする

  • # Python
  • # Django
2019年09月25日

Django REST framework でトークン認証をするメモ

トークンの作成と取得

まずは, INSTALLED_APPSrest_framework.authtoken を追加しておく

settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',  # <= 追加
    'api'
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ]
}

これでトークン用モデルが定義されたので、マイグレーションを実行する

$ python manage.py makemigrations && python manage.py migrate

トークンを取得する API エンドポイントの作成

rest_framework.authtoken にトークン取得用の view が定義されているのでルーティングをつけてあげる

import rest_framework.authtoken.views as auth_views


urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    path('api-token-auth/', auth_views.obtain_auth_token)  # <= 追加
]

トークンの作成

引数に User モデルのインスタンスを渡して、普通に作れば OK

from rest_framework.authtoken.models import Token

Token.objects.create(
    user=user  # userは User モデルインスタンス
)

ユーザーが作成されると同時にトークンも自動生成するようにすることが望ましいので、ユーザーモデルをカスタマイズして自動的にトークンを作るようにしておく

カスタムユーザーの定義

トークンと紐付けるカスタムユーザーを, AbstarctBaseUser から定義していく

トークン認証に使うユーザーモデルは, settings.py

# config/settings.py
AUTH_USER_MODEL = 'api.User'

と指定しておく必要がある

ただこうすると、管理ページへのログインもこの User を使うようになるので、 superuser 関連の設定もしておく

カスタムユーザーモデル

models.py
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.contrib.auth.models import PermissionsMixin
from django.utils.translation import gettext_lazy as _
from django.db.models.signals import post_save
from rest_framework.authtoken.models import Token
from typing import Any, Optional

from django.conf import settings


# ユーザーを作成した後に実行される
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender: str, instance: Optional['User'] = None, created: bool = False, **kwargs: Any) -> None:
    if created and instance is not None:
        # トークンの作成と紐付け
        Token.objects.create(user=instance)


class UserManager(BaseUserManager):
    def create_user(self, email: str, password: str) -> 'User':
        user = User(
            email=BaseUserManager.normalize_email(email),
        )
        user.set_password(password)
        user.save(using=self._db)

        # トークンの作成
        Token.objects.create(user=user)
        return user

    def create_superuser(self, email: str, password: str) -> 'User':
        u = self.create_user(email=email,
                             password=password)
        u.is_staff = True
        u.is_superuser = True
        u.save(using=self._db)
        return u


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(
        unique=True,
        blank=False
        )
    password = models.CharField(
        _('password'),
        max_length=128
        )
    is_staff = models.BooleanField(default=False)
    is_superuser = models.BooleanField(default=False)

    EMAIL_FIELD = 'email'
    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    objects = UserManager()

あとは普通に、serializerview とルーティングを書く

serializers.py
from api.models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ('pk', 'email', 'password')
        extra_kwargs = {
            'password': {'write_only': True}
        }

    def create(self, validated_data):
        return User.create_user(
            email=validated_data['email'],
            password=validated_data['password']
        )
views.py
from api.serializers import UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
urls.py
from rest_framework import routers
from .views import UserViewSet

router = routers.DefaultRouter()
router.register('users', UserViewSet)

これで完成

マイグレーションすれば、

$ python manage.py makemigrations && python manage.py migrate

また、既存のユーザーには

$ python manage.py shell
In [1]: from api.models import User
In [2]: from rest_framework.authtoken.models import Token
In [3]: for user in User.objects.all():
            try:
                Token.objects.create(
                    user=user
                )
            except Exception:
                pass

こんな感じで付与できる

トークンの取得

開発サーバーを建てた状態で, httpie で API を叩いてみる

$ http POST http://127.0.0.1:8000/api-token-auth/ username=[email protected] password=hoge
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Date: Mon, 25 May 2020 03:55:24 GMT
Server: WSGIServer/0.2 CPython/3.7.2
Vary: Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

ここで、username は、User モデルにて USERNAME_FIELD に規定したもの

class User(AbstractBaseUser, PermissionsMixin):
    ...
    USERNAME_FIELD = 'email'
    ...

トークン認証

まだトークン認証はできるようになったが、各エンドポイントには認証なしでアクセスできるという状態なので、パーミッションクラスを書き換えておく

settings.py
REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication'
    ],
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
}

基本的にはトークン認証だけで良いが、DRF では各エンドポイントにアクセスしたときに API リファレンス(っぽいもの)が見れるので、セッションベースの認証を追加しておくとフロントエンド開発がしやすい(API の各エンドポイントをブラウザで叩くことで実際のレスポンスが見られる)

これで, 全ての view のパーミッションがデフォルトが認証済みユーザー指定になった

認証いらずの View は、

from rest_framework.permissions import AllowAny


class HogeViewSet(viewsets.ModelViewSet):
    permission_classes = [AllowAny]

のように各ビューで上書きできる