[Django REST Framework] [React] simpleJWTによるユーザー認証①


実行環境

MacOS BigSur -- 11.2.1
Python3 -- 3.8.2
Django -- 3.1.7
djangorestframework -- 3.12.2
djoser -- 2.1.0
djangorestframework-simplejwt -- 4.6.0
npm -- 6.14.4
react -- 17.0.1
react-dom -- 17.0.1
axios -- 0.21.1

DRFとReactによるアプリケーションでJWT認証を実装したい

前回の記事⇨
https://qiita.com/kachuno9/items/ba10d81cbed6536a560b
でAPI連携部分・詳細表示などはある程度終わっているのでその続きからです。

今回、DRFとReactでアプリケーションの作成を進めていましたが、バックエンドとフロントエンドを分けることで認証周りの実装が難しく後回しにしていました。しかし認証ができなければ何も進まないので試行錯誤しつつ、JWT認証を実装したのでその手順を残したいと思います。

認証方法の選定

DRFで実装可能な認証方法はいくつかあり、採用に検討した認証方法は以下の通りです。

  • トークン認証

    • DRF標準サポート
    • Token(webサービスを利用する際に、webサーバーがユーザーを認証するために払い出した認証情報)を用いて認証を行う
    • サーバー側でランダムな文字列を発行して、管理用テーブルと照合
    • トークンに有効期限が設定できないためセキュリティ上よろしくない
  • Cookie認証

    • DRF標準サポート
    • サーバー側の「セッション」と呼ばれる保存領域(データベースなど)にユーザーのセッション情報を保存し、セッション情報を特定するID(セッションID)をwebブラウザ側のCookieに保存する
    • その後はリクエストヘッダにセッションIDをセットして毎回送信することでサーバー側でログインユーザーを特定してログイン状態を判断するという仕組み
    • サーバー側でランダムな文字列を発行して、管理用テーブルと照合
  • JWT(JSON Web Token)認証

    • DRF標準サポート外だがサードパーティ製の認証クラスを使用することで実装可能
    • 認証情報を含んだJSON形式のデータをHTTPヘッダで送信できるようエンコードしたトークン
    • 署名を含んでいるため改竄検知可能(署名されているものの暗号化されている訳ではないので、パスワードなどの機密情報をJWTに含めない)
    • トークン自体に認証情報が含まれており直接ユーザーを特定できるため、トークンをデータベースに保存する必要がない(RESTful)
    • トークンに有効期限が設定できる

ネット上の色んな記事を見ると、RESTfulなAPIではJWT認証が良いとされており、今回はJWT認証を採用しました。
その理由の一つとして、Cookie認証やトークン認証ではサーバー側でランダムな文字列を発行して管理用テーブルと照合しており、ユーザーの状態をサーバー側で持つのはRESTが提唱する「ステートレス」の概念に反することが挙げられます。
JWT認証はRESTfulなのですが、実装が少し複雑になるというデメリットも存在します。ですが、web上にも分かりやすい記事がいくつか載っており(先人の方々に感謝です..)、今回はJWT認証の中でもDRF公式で推しているdjango-rest-framework-simplejwtを用いて進めていきます。

ちなみに今回はバックエンド側の実装を行いました。フロント側は次回の記事で書こうと思います!

必要なライブラリのインストール

今回必要になる各ライブラリを以下の様にインストールします。(DRF)

$ pip3 install djoser
$ pip3 install djangorestframework_simplejwt

ちなみに、参考にした記事ではsimplejwtのインストールは以下のコマンドで行っていましたが、なぜか私の場合"_"にしないとエラーが出ました。。

$ pip3 install djangorestframework-simplejwt

DRF(バックエンド)

settings.py

変更したところを一部抜粋です。

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

# 追加
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
        #Simple JWTを読み込む
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ]
}

SIMPLE_JWT = {
    #トークンの時間を5分に設定
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=14),
    'ROTATE_REFRESH_TOKENS': True,
    'BLACKLIST_AFTER_ROTATION': False,
    #暗号のアルゴリズム設定
    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUTH_HEADER_TYPES': ('JWT',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
}

serializers.py

serializers.py
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer #追加

#トークンを発行するためのクラス
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):

    @classmethod
    def get_token(cls, user):
        token = super(MyTokenObtainPairSerializer, cls).get_token(user)

        # Add custom claims
        return token

views.py

views.py
from .serializers import MyTokenObtainPairSerializer #追加

#追加
class ObtainTokenPairWithColorView(TokenObtainPairView):
    serializer_class = MyTokenObtainPairSerializer

urls.py

my_api/urls.py(プロジェクト直下のurls.py)
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('backend.urls')),
    #追加
    #api/authアプリケーションのURLconf読み込み
    path('api/v1/auth/', include('djoser.urls.jwt'))
]

path('api/v1/auth/', include('djoser.urls.jwt'))
で認証のトークンのURLを追加しています。実際にアクセスする際のURLは、「api/v1/auth/jwt/create」です。

これらの記述を各ファイルに追加し、http://localhost:8000/api/v1/auth/jwt/create
にアクセスすると見事!このような画面で、Emailとパスワードを入力すると、アクセストークンとリフレッシュトークンが表示されました。

ターミナルでのトークン認証動作確認

認証前

$ curl http://127.0.0.1:8000/api/v1/post/

このコマンドでpostデータを取り出そうとしても、まだ認証されていないので

{"detail":"認証情報が含まれていません。"}

と返ってきます。ちなみに、今回はpostデータを指定していますが、api/v1/以降のURLは任意です。

JWT取得

curl http://127.0.0.1:8000/api/v1/auth/jwt/create -H "Accept:application/json" -H "Content-type: application/json" -X POST -d '{"email":"[email protected]","password":"sample"}'

このようにemailとpassword(今回はカスタムユーザーモデルでこの2つによる認証にしている)を指定してやると

{"refresh":"eyJ0.........",
"access":"eyJ0........."}

とJWTが長々と返ってきます。

JWT認証

取得したトークンを指定し、もう一度APIにアクセスします。
ちなみに、上で"refresh"と"access"のトークンを取得しましたが、"access"の方のトークンを指定してあげます。

$ curl http://127.0.0.1:8000/api/v1/post/ -H 'Authorization: JWT eyJ0...(トークン)'

見事!!データベースに保存しているpostデータが返ってきました^^
ちなみに、この動作確認はChromeの拡張機能であるModHeaderでも同じように確認できます。

バックエンドでの実装はここまでで完了なので、
次回はReact側の実装をしたいと思います!!!

参考

以下のページが非常に分かりやすく、参考にさせていただきました。
- https://laptrinhx.com/react-django-jwttokunwo-shitte-hui-yuan-zhuan-yongsaitowo-zuoru-676230188/
- https://day-journal.com/memo/try-036/