みなさんこんにちは!ヒロポンです!
今日はDjangoRestFrameworkでSimpleJWTを使った認証をする場合、パスワードをハッシュ化しないと正常に動かない!
という問題について書いていこうと思います。
いやハッシュ化しないと正常に動かないのではなく、ハッシュ化して認証するのが通常なので、ハッシュ化していないと正常に動かないってニュアンスかなーー。。
まあいいや!本題
※結論を見たい方は「これで安心かと思いきや」って箇所を見てくれればと思います!
DRFで単純なSimpleJWTの導入は簡単だが
DjangoRestFrameworkでのSimpleJWTの導入は非常に簡単です。
まずは下記パッケージを導入
- djangorestframework-simplejwt
- djoser
です。
下記コマンドでインストールします。
pip install djangorestframework-simplejwt
pip install djoser
で、設定ファイルにいろいろ文言を追加します。
ここでいう設定ファイルとは、
django-admin startproject <プロジェクト名> .
で作成したプロジェクトファイル直下にある、setting.pyの中です。
まずはINSTALLED_APPSに下記を追加
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'core.apps.CoreConfig',
'todo.apps.TodoConfig',
'user.apps.UserConfig',
'rest_framework',
'djoser', # jwtの認証エンドポイントを追加するためのライブラリ
]
下記を追加
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
# デフォルトの認証をJWTにするSimple JWTの読み込み
'rest_framework_simplejwt.authentication.JWTAuthentication',
],
}
JWTの設定を追加
# Jwt_tokenを使うときのおまじない
SIMPLE_JWT = {
#トークンをJWTに設定
'AUTH_HEADER_TYPES': ('JWT',),
#トークンの有効時間の設定
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60)
}
これだけで終わります。
っで、次にやるのはルーティング。
直下のurls.pyを下記のようにします。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/auth/', include('djoser.urls.jwt')),
]
別にapi/auth/の箇所はわかりやすいパスならなんでもよいと思います。
このパスを追加することで自動的に下記のパスが追加されます。
- api/auth/jwt/create/ => トークン作成
- api/auth/jwt/refresh/ => トークン再作成
- api/auth/jwt/verify => トークン検証
ここで使用するのは「api/auth/jwt/create/」が主になるかと思います。
後は適当にモデルとシリアライザーとviewを作って、認証が無いとviewが見れないようにします。
TodoのModel、シリアライザー、View
Model、シリアライザー、Viewを用意するのがめんどくさい人ようにこちらで用意しますね!
まずはログインユーザーに紐づいたTodoModel
from django.db import models
# Todoはタイトル、完了フラグ、created,updatedを持っている。
class Todo(models.Model):
title = models.CharField(max_length=100)
done = models.BooleanField(default=False)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title
次にシリアライザーです。
from rest_framework import serializers
from core.models import Todo
class TodoSeriarizer(serializers.ModelSerializer):
created = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)
updated = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)
class Meta:
model = Todo
fields = ['id', 'done', 'title', 'created', 'updated']
最後にViewを作ります。
from core.models import Todo
from rest_framework import viewsets,permissions,generics
from .serializers import TodoSeriarizer
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TodoSeriarizer
permission_classes = (permissions.IsAuthenticated,)
ここで味噌なのは下記の部分です。
permission_classes = (permissions.IsAuthenticated,)
この文言でこのViewはトークン認証がされていないと見れないようになります。
マイグレーションしてみる。
では下記コマンドでマイグレーションしましょう
python manage.py makemigrations
python manage.py migrate
次にsuperuserを作ります。
python manage.py createsuperuser
作成したユーザーでログインしてみます。
python manage.py runserver
でサーバーを走らせて、api/auth/jwt/createにアクセスすると下記のような画面が出ます。

この画面で先ほど作成したsuperuserでログインします。

このようなトークンが返ってきます。ここで使用するのはaccessトークン。

ChromeのModHeaderとかのプラグインでリクエストヘッダーにトークンを載せて、Todoにアクセスするとページが見れるはずです。

こんな感じで見れますが、リクエストヘッダーからトークンを抜くと下記のようになります。

トークンを抜いて
再度Todoのurlにアクセスする。

認証されました!
これで安心かと思いきや
では次にユーザー登録した後ログインすると仮定して、UserCreate用のページからユーザーを作成しログインしてみましょう。
userのモデルはDjangoが用意してくれているものをそのまま使うとして、シリアライザーとViewを用意します。
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from rest_framework import serializers
from django.contrib.auth import get_user_model
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'password']
extra_kwargs = {'password': {'write_only': True}}
from django.shortcuts import render
from django.contrib.auth.models import User
from rest_framework import viewsets, generics, authentication, permissions
from rest_framework.exceptions import NotFound
from .serializers import UserSerializer
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
permission_classes = (permissions.IsAuthenticated,)
作ったら適当にルーティング追加します。
っで、登録してみます。

こんな感じでユーザーを登録

ちゃんと登録できた

なぜかユーザーが見つからないとエラーがでます。
adminのページで登録ができているか確認する

adminページにユーザーが登録されていることを確認します。。

登録されています。
パスワードハッシュ化すると解消される
はい前置きが長くなりましたが、今回ログインできなかった理由。
それはパスワードハッシュ化していないことが原因です。
createsuperuserで作成したユーザーをadminページで見てみるとパスワードがハッシュ化されていることが分かります。

ですが、userパスから登録したユーザーはパスワードハッシュ化されていません。

結論。これが原因です。
パスワードハッシュ化して登録するロジックはシリアライザーに書く
では、パスワードハッシュ化はどのようにするのか?
下記のようにします。
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token
from rest_framework import serializers
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
class UserSerializer(serializers.ModelSerializer):
# パスワードのハッシュ化
# この文言でハッシュ化しないと、認証に通らない。
def validate_password(self, value: str) -> str:
return make_password(value)
class Meta:
model = User
fields = ['id', 'username', 'password']
extra_kwargs = {'password': {'write_only': True}}
これでuserパスでユーザーを作成した後、そのままログインしてトークンを取得できるはずです。
コメント