Django Rest Frameworkでトークン認証を実装する【Simple JWT】

Django

こんにちは、コウキです。

Django Rest Framework(DRF)でログイン機能の実装しようとすると、自分で実装するか、パッケージを入れて実装することになると思います。

自分で実装しても良いですが、パッケージを入れるととても簡単に実装できるのでおすすめです。
今回はsimple JWTというパッケージを使います。これにはJWTという仕様のトークンが使われています。

JWTを使った認証パッケージは他にもありますが、個人的にはsimple JWTが一番使いやすく、やりたい実装に近かったのでこちらを採用しています。

JWTトークンについて

まずはJWTトークンがどのようなものかを説明したいと思います。

JWTトークンは次のような文字列になっています。
トークンは.で3つに区切られており、1つ目がヘッダー、2つ目がペイロード、3つ目が署名となっています。

ペイロードの中には好きな情報を埋め込むことができ、user_idやトークンの有効期限などの情報を入れます。
署名はトークンが正しいものか検証するためのもので、これにより改ざんできないようになっています。
ヘッダーには署名のアルゴリズム等が記載されています。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU

トークンにはアクセストークンとリフレッシュトークンの2種類あります。
アクセストークンは実際の認証に使われ、リフレッシュトークンはアクセストークンの有効期限が切れたときに、トークンを再発行するために使います。

トークンを使った処理の流れは次のようになります。
リフレッシュトークンの有効期限が切れる前にトークン再発行をすれば、ログイン状態をずっと維持することができます。

Simple JWTのインストール

まずはsimple JWTをインストールします。

pip install djangorestframework-simplejwt

Simple JWTの導入

settings.pyでsimple JWTの認証機能を使うように設定します。
これはトークン認証を行うときに指定したクラスの認証ロジックを使用するというものです。

REST_FRAMEWORK = {
    ...
    'DEFAULT_AUTHENTICATION_CLASSES': (
        ...
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    )
    ...
}

次にurls.pyにトークン発行用のAPIのエンドポイントを追加します。
上からトークン発行、トークン再発行のエンドポイントになります。

トークン発行はユーザーログイン時に実行するもので、トークン再発行はトークンの有効期限が切れたときに使用するものです。

from rest_framework_simplejwt.views import (
    TokenObtainPairView,
    TokenRefreshView,
)

urlpatterns = [
    ...
    path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    ...
]

ひとまずここまででトークン認証はできるようになりました。

Simple JWTの設定

次にsimple JWTの設定を行います。
上からアクセストークンの有効期限、リフレッシュトークンの有効期限、トークン再発行にリフレッシュトークンを含めるか、最終ログイン日時を更新するかの設定になります。

SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=15),
    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=30),
    'ROTATE_REFRESH_TOKENS': True,
    'UPDATE_LAST_LOGIN': True,
}

トークンを発行してみる

それではトークンを実際に発行してみましょう。
userモデルでemailとpasswordでログインするように設定している場合はusernameemailに自動で変更されます。

curl -X POST "http://127.0.0.1:8000/api/token" \
    -H  "accept: application/json" \
    -H  "Content-Type: application/json" \
    -d '{"username": "user1", "password": "secret"}'

レスポンスは次のようになります。

{
"access":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiY29sZF9zdHVmZiI6IuKYgyIsImV4cCI6MTIzNDU2LCJqdGkiOiJmZDJmOWQ1ZTFhN2M0MmU4OTQ5MzVlMzYyYmNhOGJjYSJ9.NHlztMGER7UADHZJlxNG0WSi22a2KaYSfd1S-AuT7lU",
  "refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX3BrIjoxLCJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImNvbGRfc3R1ZmYiOiLimIMiLCJleHAiOjIzNDU2NywianRpIjoiZGUxMmY0ZTY3MDY4NDI3ODg5ZjE1YWMyNzcwZGEwNTEifQ.aEoAYkSJjoWH1boshQAaTkf8G3yn0kapko6HFRt7Rh4"
}

トークン再取得の場合は次のようになります。レスポンスの形式はトークン発行と同じです。

curl -X POST "http://127.0.0.1:8000/api/token/refresh/" \
    -H  "accept: application/json" \
    -H  "Content-Type: application/json" \
    -d '{"refresh": "リフレッシュトークン"}'

次はトークンを使ってAPIを実行してみます。
トークンを使用するときはauthorizationヘッダーにアクセストークンをセットします。

curl -X GET "http://127.0.0.1:8000/api/user/" -H  "accept: application/json" -H  "authorization: Bearer アクセストークン"

リフレッシュトークンを再利用できなくする

リフレッシュトークンは1度しか使用せず、有効期限も長いため、セキュリティを高めるために再利用できないようにします。
simple JWTでは簡単に追加することができます。

INSTALLED_APPS = [
  ...
    'rest_framework',
    'rest_framework_simplejwt.token_blacklist',  # これを追加
  ...
]

テーブルが追加されるのでmigrateしましょう。

python manage.py migrate

これで使用したリフレッシュトークンが再利用できなくなりました。
この設定を行うとDBにリフレッシュトークンのデータが溜まっていくので、定期的に以下のコマンドで有効期限切れのデータを削除します。

python manage.py flushexpiredtokens

データが溜まっていくと遅くなるので、本番環境では実行するようにしましょう。

drf-yasgの設定

ここからは番外編です。
drfでAPIを作成するときにdrf-yasgを導入して、swaggerを使えるようにしておくとデバッグなどをするときに便利ですが、そのままだと今回作成したトークンAPIが正しく表示されないので少し修正します。

まずはレスポンス用のSerializerを作成します。

class TokenObtainPairResponseSerializer(serializers.Serializer):
    access = serializers.CharField()
    refresh = serializers.CharField()

    def create(self, validated_data):
        raise NotImplementedError()

    def update(self, instance, validated_data):
        raise NotImplementedError()

class TokenRefreshResponseSerializer(serializers.Serializer):
    access = serializers.CharField()
    refresh = serializers.CharField()

    def create(self, validated_data):
        raise NotImplementedError()

    def update(self, instance, validated_data):
        raise NotImplementedError()

次にviewを作成します。

class DecoratedTokenObtainPairView(TokenObtainPairView):
    @swagger_auto_schema(responses={status.HTTP_200_OK: TokenObtainPairResponseSerializer})
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)

class DecoratedTokenRefreshView(TokenRefreshView):
    @swagger_auto_schema(responses={status.HTTP_200_OK: TokenRefreshResponseSerializer})
    def post(self, request, *args, **kwargs):
        return super().post(request, *args, **kwargs)

最後にurls.pyを先程作成したviewに入れ替えます。

urlpatterns = [
    ...
    path('api/token/', DecoratedTokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('api/token/refresh/', DecoratedTokenRefreshView.as_view(), name='token_refresh'),
    ...
]

これでswagger上でも正しいレスポンスが表示されるようになります。

drf-yasgのswaggerの画面でログインできるようにする

swaggerにはログインしてAPIを実行する機能がありますが、正しく設定しないとログインができません。
ログインの設定は少し複雑ですが、設定しておくと開発が捗るのでやっておくことをおすすめします。

まずはswaggerのログイン方式の設定をします。
今回はoauth2のパスワードログインを指定します。

SWAGGER_SETTINGS = {
    'USE_SESSION_AUTH': False,  # Django loginボタンを消す
    'SECURITY_DEFINITIONS': {
        'Bearer': {
            'type': 'oauth2',
            'name': 'Authorization',
            'in': 'header',
            'flow': 'password',
            'tokenUrl': '/api/swagger/login',  # swagger用ログインAPIのエンドポイント
        }
    },
}

デフォルトのログインAPIではoauth2の仕様に沿っていないため、oauth2の仕様に沿ったAPIを作成します。
まずはSerializerを作成します。基本的にはリクエストとレスポンスのインターフェイスをデフォルトのものから変更しているだけです。

class SwaggerTokenObtainSerializer(serializers.Serializer):
    username_field = get_user_model().USERNAME_FIELD

    default_error_messages = {'no_active_account': _('No active account found with the given credentials')}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.fields['username'] = serializers.CharField()
        self.fields['password'] = PasswordField()

    def validate(self, attrs):
        authenticate_kwargs = {
            self.username_field: attrs['username'],
            'password': attrs['password'],
        }
        try:
            authenticate_kwargs['request'] = self.context['request']
        except KeyError:
            pass

        self.user = authenticate(**authenticate_kwargs)

        if not api_settings.USER_AUTHENTICATION_RULE(self.user):
            raise exceptions.AuthenticationFailed(
                self.error_messages['no_active_account'],
                'no_active_account',
            )

        return {}

    @classmethod
    def get_token(cls, user):
        raise NotImplementedError('Must implement `get_token` method for `TokenObtainSerializer` subclasses')

class SwaggerLoginSerializer(SwaggerTokenObtainSerializer):
    @classmethod
    def get_token(cls, user):
        return RefreshToken.for_user(user)

    def validate(self, attrs):
        data = super().validate(attrs)

        refresh = self.get_token(self.user)

        data['refresh_token'] = str(refresh)
        data['access_token'] = str(refresh.access_token)
        data['token_type'] = 'Bearer'

        if api_settings.UPDATE_LAST_LOGIN:
            update_last_login(None, self.user)

        return data

次にviewを作成します。
このAPIはswaggerに表示する必要がないためswagger_schema = Noneで非表示にしています。
また、rendererとparserはcamelケースに変換するものを設定している場合が多いので、通常のものを設定しています。

class SwaggerLoginView(TokenObtainPairView):
    serializer_class = SwaggerLoginSerializer
    swagger_schema = None

    renderer_classes = [JSONRenderer, BrowsableAPIRenderer]
    parser_classes = [JSONParser, FormParser, MultiPartParser]

urls.pyにも追加します。

urlpatterns = [
    ...
    path('api/swagger/login', SwaggerLoginView.as_view()),
    ...
]

これでswagger上でパスワードログインができるようになりました。

おわりに

今回はDRFでトークン認証によるログイン機能を実装する方法を紹介しました。
ログイン機能は最初は難しく感じますが、慣れてくればすぐに実装できるようになると思います。
ほとんどのサービスで必要な機能なので、確実に実装できるようにしましょう。

更に細かく設定したい場合はsimple JWTの公式ドキュメントが参考になるので、参考にしてみてください。
https://django-rest-framework-simplejwt.readthedocs.io/en/latest/index.html

それでは^^

コメント

タイトルとURLをコピーしました