はじめに
続き。
バックエンドのDjangoでJWTを使ってユーザ認証を行う部分です。
これが動けばフロントエンドに取り掛かれます。
コツ
admin.pyでカスタムユーザーを扱えるように記述しないと、Djangoの管理画面で新規登録したユーザのパスワードがハッシュで暗号化されず、平文で保存されるのでJWT認証ができない。
構成
現時点でPython3.11を使うとDjangoが一部のライブラリが読み込めずにinternal Server Errorになるので、Python3.10を使います。
- Debian bullseye 64bit
- Python 3.10.13
pipenvによる仮想環境
- Django 4.2.6
- djangorestframework 3.14.0
- djangorestframework-simplejwt 5.3.0
- PyJWT 2.8.0
ディレクトリ構成
pipenvで仮想環境(.venv、Pipfile、Pipfile.lock)を作っています。
また、uWSGI(uwsgi.ini)を使ってHTTPで通信します。
.venv Pipfile Pipfile.lock db.sqlite3 uwsgi.ini manage.py config - asgi.py - settings.py - urls.py - wsgi.py user - admin.py - apps.py - migrations - models.py - serializers.py - tests.py - urls.py - views.py
カスタムユーザーのアプリ作成
「user」というカスタムユーザーのアプリを作成します。
設定ファイルはconfigディレクトリに作成します。
mkdir backend cd backend django-admin startproject config . django-admin startapp user
settings.py
「settings.py」に以下を追記します。
INSTALLED_APPS = [
省略
'rest_framework',
'rest_framework_simplejwt',
'user', # Custom User
]
# Custom User Model
AUTH_USER_MODEL = 'user.CustomUser'
REST_FRAMEWORK = {
# JWT認証
'DEFAULT_AUTHENTICATION_CLASSES': (
#'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
# 認証が必要
#'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
#],
}
SIMPLE_JWT = {
# アクセストークン(1時間)
'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
# リフレッシュトークン(3日)
'REFRESH_TOKEN_LIFETIME': timedelta(days=3),
# 認証タイプ
#'AUTH_HEADER_TYPES': ('JWT', ),
# 認証トークン
#'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken', ),
# トークン再発行にリフレッシュトークンを含める
'ROTATE_REFRESH_TOKENS': True,
# 最終ログイン日時を更新
'UPDATE_LAST_LOGIN': True,
}
models.py
カスタムユーザーモデルを定義します。
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
import uuid
class CustomUserManager(BaseUserManager):
def create_user(self, email, password=None, **extra_fields):
if not email:
raise ValueError("Users must have an email address")
user = self.model(
email = self.normalize_email(email),
**extra_fields
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None, **extra_fields):
user = self.create_user(
email,
password=password,
)
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class CustomUser(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(verbose_name='UUID', default=uuid.uuid4, primary_key=True, editable=False)
email = models.EmailField(verbose_name='メールアドレス', unique=True)
name = models.CharField(verbose_name='氏名', max_length=128)
affiliation = models.CharField(verbose_name='所属', max_length=256)
remarks = models.TextField(verbose_name='備考', blank=True, null=True)
is_active = models.BooleanField(verbose_name='アカウント有効', default=True)
is_staff = models.BooleanField(verbose_name='スタッフ権限', default=False)
created_time = models.DateTimeField(verbose_name='作成日時', auto_now_add=True)
updated_time = models.DateTimeField(verbose_name='更新日時', auto_now=True)
objects = CustomUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['password']
# 他のモデルからChargeTypeをForeignKeyで指定した場合に名前が表示されるようにする。
def __str__(self):
return self.email
# 管理画面の表示名(大項目名)を変更する。
class Meta:
verbose_name = "ユーザー"
verbose_name_plural = "ユーザー"
serializers.py
シリアライザーを記述します。
from django.contrib.auth import get_user_model
from django.contrib.auth.password_validation import validate_password
from django.core import exceptions
from rest_framework import serializers
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from django.contrib.auth.hashers import make_password
#from .models import CustomUser
CustomUser = get_user_model()
class CustomUserCreateSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
#fields = ['email', 'password']
#fields = ['id', 'email', 'password']
fields = '__all__'
extra_kwargs = {'password': {'write_only': True}}
def validate(self, data):
user = CustomUser(**data)
password = data.get('password')
try:
validate_password(password, user)
except exceptions.ValidationError as e:
serializer_errors = serializers.as_serializer_error(e)
raise exceptions.ValidationError(
{'password': serializer_errors['non_field_errors']}
)
return data
def create(self, validated_data):
user = CustomUser(
#user = CustomUser.objects.create_user(
email = validated_data['email'],
password = validated_data['password']
)
ser.set_password(validated_data['password'])
user.save()
return user
class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
# ここを変更すると、JWTで取得できる情報の種類を変更可能
fields = ['email']
#fields = '__all__'
#def validate_password(self, value:str) ->str:
# # ハッシュ値に変換する
# return make_password(value)
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
data = super(CustomTokenObtainPairSerializer, self).validate(attrs)
user = CustomUserSerializer(self.user)
data.update({'user': user.data})
return data
views.py
各クラスを定義します。
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.views import TokenObtainPairView
from .serializers import CustomUserCreateSerializer, CustomUserSerializer, CustomTokenObtainPairSerializer
class Register(APIView):
def post(self, request):
data = request.data
serializer = UserCreateSerializer(data=data)
if not serializer.is_valid():
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
user = serializer.create(serializer.validated_data)
refresh = RefreshToken.for_user(user)
user = UserSerializer(user)
data = {
"refresh": str(refresh),
"access": str(refresh.access_token),
"user": user.data
}
return Response(data, status=status.HTTP_201_CREATED)
class VerifyUser(APIView):
permissions_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
user = UserSerializer(user)
data = {"user": user.data}
return Response(data, status=status.HTTP_200_OK)
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
urls.py
URLを設定します。
from django.urls import path
from .views import Register, VerifyUser, MyTokenObtainPairView
from rest_framework_simplejwt.views import TokenRefreshView
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('signup', Register.as_view()),
path('login', MyTokenObtainPairView.as_view()),
path('token/refresh', TokenRefreshView.as_view()),
path('token/verify', VerifyUser.as_view()),
# test
# https://zenn.dev/tanaka_gaku/articles/b2b99acd100792
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
admin.py
管理画面でカスタムユーザーを扱うために記述します。
これらを追記しないと情報の表示、新規ユーザの追加などが行えません。
from django.contrib import admin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
from django.contrib.auth import get_user_model
#from .models import CustomUser
CustomUser = get_user_model()
class CustomUserChangeForm(UserChangeForm):
class Meta(UserChangeForm.Meta):
model = CustomUser
fields = (
'email',
'name',
'affiliation',
'remarks',
'is_staff',
'is_active',
)
class CustomUserAdmin(DefaultUserAdmin):
# 保存ボタンの配置を上部に変更
save_on_top = True
# デフォルトの「保存して1つ追加」を「新規保存」に変更。
# 追加されているデータを元に新しくデータを保存する。
save_as = True
form = CustomUserChangeForm
add_form = DefaultUserAdmin.add_form
model = CustomUser
# 一覧で表示する項目
list_display = (
'email',
'name',
'affiliation',
'remarks',
'is_staff',
'is_active',
"is_superuser",
)
# CustomUserモデルで定義されている不要なフィールド(usernameなど)を定義しないようにする。
ordering = ("email",)
filter_horizontal = (
"groups",
"user_permissions",
)
# ユーザ情報で表示する項目
fieldsets = (
(None, {'fields': ('email', 'password',)}),
('Personal info', {'fields': ('name', 'affiliation', 'remarks',)}),
('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions',)}),
('Important dates', {'fields': ('last_login',)}),
)
# ユーザーの新規追加に必要なフィールド
# password1、password2は必須。
# 参考:https://stackoverflow.com/questions/65049506/unknown-fields-username-specified-for-user-check-fields-fieldsets-exclude-a
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': (
'email',
'name',
'affiliation',
'remarks',
'password1',
'password2',
),
}),
)
admin.site.register(CustomUser, CustomUserAdmin)
データベース反映
データベースに反映させます。
python manage.py makemigrations python manage.py migrate
ユーザの作成
管理ユーザを作成します。
また、管理画面でテストユーザを作成します。
python manage.py createsuperuser
パスワードのハッシュ化を確認する
データベースの登録状況と、パスワードがハッシュ化されているかを確認します。
ハッシュ化されていないと、curlでユーザ情報を取得できません。
テストなのでSQLiteを使います。
「.tables」でユーザ情報が保存されているテーブル名を取得して、「select * from user_customuser;」で中身を確認します。
sudo apt install sqlite3 python manage.py dbshell .tables select * from user_customuser;
PostgreSQLの場合は、「\d」でユーザ情報が保存されているテーブル名を取得して、「select * from user_customuser;」で中身を確認します。
sudo apt install psycopg2 python manage.py dbshell \d select * from user_customuser;
JWTの接続テスト
テストサーバーを起動します。
テストサーバなのでローカルのみしかアクセスできませんが、この記事を参考にして、uWSGIをHTTP通信で動かせば外部からもテストできます。
python manage.py runserver
curlで情報を取得できることを確認します。
管理者(kanri@hoge.com、パスワード;abcd)の場合、
curl -X POST "http://127.0.0.1:8000/user/api/token" -H "accept: application/json" -H "Content-Type: application/json" -d '{"email": "kanri@hoge.com", "password": "abcd"}'
curl -X POST "http://127.0.0.1:8000/user/login" -H "accept: application/json" -H "Content-Type: application/json" -d '{"email": "kanri@hoge.com", "password": "abcd"}'
テストユーザ(test@hoge.com、パスワード;abcd)の場合、
curl -X POST "http://127.0.0.1:8000/user/api/token" -H "accept: application/json" -H "Content-Type: application/json" -d '{"email": "test@hoge.com", "password": "abcd"}'
curl -X POST "http://127.0.0.1:8000/user/login" -H "accept: application/json" -H "Content-Type: application/json" -d '{"email": "test@hoge.com", "password": "abcd"}'



Comments