0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JWT認証【Django】

Last updated at Posted at 2025-02-21

はじめに

この記事ではDjangoの中でJWT認証を行う方法、今回はSimpleJWTを取り上げます。
また、Djangoについての基本的な知識があることを前提としています。
Django初心者の方向けに記事を貼っておきます。

JWT認証とは

詳細はこちらの記事に譲る

DjangoでJWT認証を使う

DjangoRestFramework(以下、DRF)公式で紹介されているSimpleJWTを用いた認証を実装していく

セットアップ

まず、パッケージをインストールする

$ pip install djangorestframework-simplejwt

settings.pyにライブラリを使用する設定を追記する
下はREST APIの認証時にデフォルトでJWTAuthenticationを使う設定の追加

settings.py
REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES' : ['rest_framework.permissions.IsAuthenticated']
}

URLマッピング

urls.pyにトークンの発行・再発行を行うビューを登録する

urls.py
from restframework_simplejwt.views import TokenObtainPairView, TokenRefreshView

urlpatterns = [

    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

SimpleJWTの組み込みViewを使用しているので、View.pyへの追加は不要

ユーザ情報の追加

認証に使用するユーザ情報を作成する。ここでは、独自のユーザテーブルは作成せず、Djangoの組み込みユーザテーブルを使用する。

$ python manage.py createsuperuser

トークン発行確認

http://localhost:8000/.../token/にアクセスし、認証情報をPOSTしたときにaccessrefreshのトークンが得られるはず
もし{ "detail": "No active account found with the given credentials" }のようなレスポンスであれば、POSTしたユーザ情報が正しいか見直そう

  • アクセストークン
    通常はリクエストヘッダのAuthorizationフィールドにBearerトークンとしてセットして、リクエストに付与する。
    サーバはこのトークンを検証して、有効であればアクセスを許可する。
  • リフレッシュトークン
    アクセストークンの有効期限が切れた場合に再発行するためのトークン
    リフレッシュトークンの有効期限はアクセストークンよりも長く設定される

トークンのカスタマイズ

トークンの有効時間や含める情報を設定する

settings.py
from timedelta import datetime
from pathlib import path

SIMPLE_JWT = {

    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=10), # 有効時間10分
    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=30), # 有効時間30日
    'UPDATE_LAST_LOGIN': True, # ログイン時にauth_userテーブルにlast_loginフィールドを更新する
}

timedeltaがインストールされていない場合は以下でインストールできる

$ pip install timedelta

認証の制御(Viewの実装)

views.py
from rest_framework import generics, status, views, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework.views import APIView

class ExampleView(APIView):

    # 認証クラスの指定
    authentication_classes = [ JWTAuthentication ]

    # アクセス許可の指定
    permission_classes = [ IsAuthenticated ]

    def get(self, request):
        pass

    def post(self, request):
        pass

アクセス許可指定

  • 認証済みのリクエストのみ許可するとき
    authentication_classes = [ JWTAuthentication ]
    permission_classes = [ IsAuthenticated ]

  • すべてのユーザにアクセス許可するとき(ログイン処理など)
    authentication_classes = [ ]
    permission_classes = [ ]

認証トークンの保存

アクセストークンの自動セット

よくある実装
フロントエンドからのリクエストの際、にヘッダーに認証トークンを付与する

今回は、バックエンドで自動的にセットする認証機構を取りあげる

アプリディレクトリ配下にauthentication.pyを作成する

認証トークンハンドリング(authentication.py)
from rest_framework_simplejwt.authentication import JWTAuthentication

class CustomJWTAuthentication(JWTAuthentication):

    def get_header(self, request):

        token = request.COOKIES.get('access')
        request.META['HTTP_AUTHORIZATION'] = '{header_type} {access_token}'.format(header_type="Bearer", access_token=token)

        refresh = request.COOKIES.get('refresh')
        request.META['HTTP_REFRESH_TOKEN'] = refresh

        return super().get_header(request)

ブラウザがサードパーティクッキー拒否設定になっていると認証ができないことがある

ログイン処理

トークンをCookieに保存

認証の結果、取得したトークンをCookieに保存するLoginViewクラスを追加する

views.py
from django.conf import settings
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework.views import APIView

class LoginView(APIView):

    """ユーザのログイン処理
    
    Args:
        APIView (class): rest_framework.viewのAPIViewを受け取る
    """

    authentication_classes = [ JWTAuthentication ]
    permission_classes = [ ]

    def post(self, request):

        serializer = TokenObtainPairSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        access = serializer.validated_data.get("access", None)
        refresh = serializer.validated_data.get("refresh", None)

        if access:
            response = Response(status=status.HTTP_200_OK)
            max_age = settings.COOKIE_TIME
            response.set_cookie('access', access, httponly=True, max_age=max_age)
            response.set_cookie('refresh', refresh, httponly=True, max_age=max_age)

            return response

        return Response({'errMsg': 'ユーザの認証に失敗しました'}, status=status.HTTP_401_UNAUTHORIZED)

LoginViewをURLマッピングに追加する

urls.py(ログインビューの追加)
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from . import views

urlpatterns = [

    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("login/", views.LoginView.as_view()), # 追加
]

カスタムJWTとクッキーの有効期限を設定する

settings.py(追記)
REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'xxx.authentication.CustomJWTAuthentication', # 追加 (xxx: authentication.pyを作成したディレクトリ)
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES' : ['rest_framework.permissions.IsAuthenticated']
}

# クッキーの有効期限の設定
COOKIE_TIME = 60 * 60 * 12 # 追加

トークン再発行

views.py(追記)
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer
from rest_framework.views import APIView

class RetryView(APIView):

    authentication_classes = [ JWTAuthentication ]
    permission_classes = []

    def post(self, request):

        request.data['refresh'] = request.META.get('HTTP_REFRESH_TOKEN')
        serializer = TokenRefreshSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        access = serializer.validated_data.get('access', None)
        refresh = serializer.validated_data.get('refresh', None)

        if access:
            response = Response(status=status.HTTP_200_OK)
            max_age = settings.COOKIE_TIME
            response.cookie_set('access', access, httponly=True, max_age=max_age)
            response.cookie_set('refresh', refresh, httponly=True, max_age=max_age)

            return response

        return Response({'errMsg': 'ユーザの認証に失敗しました'}, status=status.HTTP_401_UNAUTHORIZED)
urls.py
from restframework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from . import views

urlpatterns = [

    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("login/", views.LoginView.as_view()),
    path("retry/", views.RetryView.as_view()), # 追加
]

ログアウト処理

views.py(追記)
class LogoutView(APIVIew):

    authentication_classes = []
    permission_classes = []

    def post(self, request, *args):

        response = Response(status=status.HTTP_200_OK)
        response.delete_cookie('access')
        response.delete_cookie('refresh')

        return response
urls.py
from restframework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from . import views

urlpatterns = [

    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("login/", views.LoginView.as_view()),
    path("retry/", views.RetryView.as_view()), 
    path("logout/", views.LogoutView.as_view()), # 追加
]

全体コード

settings.py(追記部抜粋)
from timedelta import datetime
from pathlib import path

REST_FRAMEWORK = {

    'DEFAULT_AUTHENTICATION_CLASSES': (
        'xxx.authentication.CustomJWTAuthentication', # (xxx: authentication.pyを作成したディレクトリ)
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES' : ['rest_framework.permissions.IsAuthenticated']
}

# クッキーの有効期限の設定
COOKIE_TIME = 60 * 60 * 12

SIMPLE_JWT = {

    'ACCESS_TOKEN_LIFETIME': datetime.timedelta(minutes=10), # 有効時間10分
    'REFRESH_TOKEN_LIFETIME': datetime.timedelta(days=30), # 有効時間30日
    'UPDATE_LAST_LOGIN': True, # ログイン時にauth_userテーブルにlast_loginフィールドを更新する
}
views.py
from django.conf import settings
from rest_framework import generics, status, views, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer, TokenRefreshSerializer
from rest_framework.views import APIView

class LoginView(APIView):

    """ユーザのログイン処理
    
    Args:
        APIView (class): rest_framework.viewのAPIViewを受け取る
    """

    authentication_classes = [ JWTAuthentication ]
    permission_classes = [ ]

    def post(self, request):

        serializer = TokenObtainPairSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        access = serializer.validated_data.get("access", None)
        refresh = serializer.validated_data.get("refresh", None)

        if access:
            response = Response(status=status.HTTP_200_OK)
            max_age = settings.COOKIE_TIME
            response.set_cookie('access', access, httponly=True, max_age=max_age)
            response.set_cookie('refresh', refresh, httponly=True, max_age=max_age)

            return response

        return Response({'errMsg': 'ユーザの認証に失敗しました'}, status=status.HTTP_401_UNAUTHORIZED)



class RetryView(APIView):

    authentication_classes = [ JWTAuthentication ]
    permission_classes = []

    def post(self, request):

        request.data['refresh'] = request.META.get('HTTP_REFRESH_TOKEN')
        serializer = TokenRefreshSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        access = serializer.validated_data.get('access', None)
        refresh = serializer.validated_data.get('refresh', None)

        if access:
            response = Response(status=status.HTTP_200_OK)
            max_age = settings.COOKIE_TIME
            response.cookie_set('access', access, httponly=True, max_age=max_age)
            response.cookie_set('refresh', refresh, httponly=True, max_age=max_age)

            return response

        return Response({'errMsg': 'ユーザの認証に失敗しました'}, status=status.HTTP_401_UNAUTHORIZED)



class LogoutView(APIVIew):

    authentication_classes = []
    permission_classes = []

    def post(self, request, *args):

        response = Response(status=status.HTTP_200_OK)
        response.delete_cookie('access')
        response.delete_cookie('refresh')

        return response
authentication.py
from rest_framework_simplejwt.authentication import JWTAuthentication

class CustomJWTAuthentication(JWTAuthentication):

    def get_header(self, request):

        token = request.COOKIES.get('access')
        request.META['HTTP_AUTHORIZATION'] = '{header_type} {access_token}'.format(header_type="Bearer", access_token=token)

        refresh = request.COOKIES.get('refresh')
        request.META['HTTP_REFRESH_TOKEN'] = refresh

        return super().get_header(request)
urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from . import views

urlpatterns = [

    path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
    path("login/", views.LoginView.as_view()),
    path("retry/", views.RetryView.as_view()), 
    path("logout/", views.LogoutView.as_view()),
]

以上の実装後、ユーザ定義関数・クラスでの認証は以下のようにCustomJWTAuthenticationを使用する

ユーザ定義
class ExampleView(APIView):

    # 認証クラスの指定
    authentication_classes = [ CustomJWTAuthentication ]

    # アクセス許可の指定
    permission_classes = [ IsAuthenticated ]

    def get(self, request):
        pass

    def post(self, request):
        pass
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?