はじめに
続き。
バックエンドの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