はじめに
この記事ではDjangoの中でJWT認証を行う方法、今回はSimpleJWTを取り上げます。
また、Djangoについての基本的な知識があることを前提としています。
Django初心者の方向けに記事を貼っておきます。
JWT認証とは
詳細はこちらの記事に譲る
DjangoでJWT認証を使う
DjangoRestFramework(以下、DRF)公式で紹介されているSimpleJWTを用いた認証を実装していく
セットアップ
まず、パッケージをインストールする
$ pip install djangorestframework-simplejwt
settings.py
にライブラリを使用する設定を追記する
下はREST APIの認証時にデフォルトでJWTAuthenticationを使う設定の追加
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES' : ['rest_framework.permissions.IsAuthenticated']
}
URLマッピング
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したときにaccess
とrefresh
のトークンが得られるはず
もし{ "detail": "No active account found with the given credentials" }
のようなレスポンスであれば、POSTしたユーザ情報が正しいか見直そう
- アクセストークン
通常はリクエストヘッダのAuthorizationフィールドにBearerトークンとしてセットして、リクエストに付与する。
サーバはこのトークンを検証して、有効であればアクセスを許可する。 - リフレッシュトークン
アクセストークンの有効期限が切れた場合に再発行するためのトークン
リフレッシュトークンの有効期限はアクセストークンよりも長く設定される
トークンのカスタマイズ
トークンの有効時間や含める情報を設定する
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の実装)
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
を作成する
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クラスを追加する
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マッピングに追加する
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とクッキーの有効期限を設定する
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 # 追加
トークン再発行
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)
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()), # 追加
]
ログアウト処理
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
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()), # 追加
]
全体コード
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フィールドを更新する
}
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
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)
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