LoginSignup
0
0

Django REST frameworkとReactでJWT認証

Posted at

はじめに

個人開発で、DRFとReactで認証処理を作成しました。
普段は日記がてら投稿しているのですが、ある程度まとめたものを投稿しようと思います。

認証関連の記事

インストール

dj-rest-authとdjangorestframework-simplejwtをインストール。
dj-rest-authは簡単に認証処理をカスタマイズできるライブラリで、
djangorestframework-simplejwtは、簡単にJWT認証を実装できるライブラリです。

pip install dj-rest-auth djangorestframework-simplejwt

settings.pyを修正

settings.py
INSTALLED_APPS = [
    ...
+    'rest_framework.authtoken',
]

+ AUTH_USER_MODEL = 'user.User'

REST_FRAMEWORK = {
+    'DEFAULT_AUTHENTICATION_CLASSES': (
+        'rest_framework_simplejwt.authentication.JWTAuthentication',
+    ),
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

+ # dj-rest-authでJWT認証をする設定
+ REST_AUTH = {
+     "USE_JWT": True,
+ }

+ # JWTの設定
+ # 以下ではJWTの期限を5分に設定している
+ SIMPLE_JWT = {
+     'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
+ }

以下分については、次に作成するUserモデルを指定しています。

AUTH_USER_MODEL = 'user.User'

modelを作成

models.py
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.db import models
from django.contrib.auth.models import PermissionsMixin


class UserManager(BaseUserManager):
    def create_user(self, email, password=None, **extra_fields):
        if not email:
            raise ValueError("The Email field must be set")
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        return self.create_user(email, password, **extra_fields)


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(unique=True)
    first_name = models.CharField(max_length=30, blank=True)
    last_name = models.CharField(max_length=30, blank=True)
    bio = models.CharField(max_length=280, blank=True)
    location = models.CharField(max_length=100, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    # superuserを作成する際にemail以外で追加で必須にする項目を定義
    REQUIRED_FIELDS = []

    def __str__(self):
        return self.email

Userはメールアドレス、パスワードで認証したいため、AbstractBaseUserをします。

serializerを作成

serializer.py
from dj_rest_auth.serializers import LoginSerializer
from django.contrib.auth import authenticate
from rest_framework import serializers

from .models import User


class UserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, required=False)

    class Meta:
        model = User
        fields = ('email', 'password', 'first_name', 'last_name', 'bio', 'location', 'birth_date')

    def create(self, validated_data):
        # パスワードを validated_data から削除し、ユーザー作成後に設定する
        password = validated_data.pop('password', None)
        user = User.objects.create_user(**validated_data)
        if password is not None:
            user.set_password(password)
            user.save()
        return user


class CustomLoginSerializer(LoginSerializer):
    username = None
    email = serializers.EmailField(required=True)

    def get_auth_user(self, username, email, password):
        user = authenticate(username=email, password=password)
        if not user:
            raise serializers.ValidationError("Invalid email or password")
        return user

UserSerializerについて

UserSerializerは主にUserをCURDする際に使います。

CustomLoginSerializerについて

こちらはdj_rest_authで用意されている、LoginSerializerを継承しています。
今回はdjangoのデフォルトのusernameではなく、emailで認証したいため、カスタマイズしています。
dj_rest_authでならちょっと書くだけで、簡単にカスタマイズできます。

viewsを作成

dj_rest_authのLoginViewを使いたいため、ログインはViewSetを使用しません。
dj_rest_authのLoginViewは中身を見るとよりわかるのですが、認証関連をいろいろやってくれています。
ViewSetではUser作成後にアクセストークンを返すようにしたいため、createメソッドをカスタマイズしています。

views.py
from dj_rest_auth.views import LoginView
from rest_framework import viewsets
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from rest_framework_simplejwt.tokens import RefreshToken

from .serializer import UserSerializer, CustomLoginSerializer
from .models import User


# ユーザのCRUD操作を行うViewSet
class UserModelViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    def get_permissions(self):
        if self.action == "create":
            permission_classes = [AllowAny]
        else:
            permission_classes = [IsAuthenticated]
        return [permission() for permission in permission_classes]

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        if serializer.is_valid():
            user = serializer.save()  # userインスタンスを保存して取得
            refresh = RefreshToken.for_user(user)  # 新規ユーザーのためのトークン生成
            response_data = {
                'user': serializer.data,
                'refresh': '',  # refreshは使用しない
                'access': str(refresh.access_token),
            }
            return Response(response_data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)


class CustomLoginView(LoginView):
    serializer_class = CustomLoginSerializer

React側を作成

ここでは全体は書かずにJWTにかかわる部分のみを書きます。

ユーザー登録画面を作成

ユーザー登録後にアクセストークンをローカルストレージに保存します。

RegisterForm.jsx
const registerUser = (data) => {
    const jsonData = JSON.stringify(data);
    const config = {
        headers: {
            'Content-Type': 'application/json'
        }
    };
    const url = `${API_URL}/api/users/`;
    return axios.post(url, jsonData, config)
};

export const RegisterForm = () => {
  const handleSubmit = (event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);

    const registrationData = {
      email: data.get('email'),
      password: data.get('password'),
      first_name: data.get('firstName'),
      last_name: data.get('lastName')
    };

    registerUser(registrationData)
      .then((response) => {
        if (response.status === 201) {
            localStorage.setItem('access_token', response.data.access);
            localStorage.setItem('user', JSON.stringify(response.data.user));
        }
      });
  };

ログイン画面を作成

こちらもログイン完了後にアクセストークンをローカルストレージに保存します。

LoginForm.jsx
const login = (data) => {
    const jsonData = JSON.stringify(data);
    const config = {
        headers: {
            'Content-Type': 'application/json',
        }
    };
    const url = `${API_URL}/api/user/login/`;
    return axios.post(url, jsonData, config)
};

export const LoginForm = () => {
  const handleSubmit = (event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);

    const loginData = {
      email: data.get('email'),
      password: data.get('password'),
    };

    login(loginData)
      .then((response) => {
        if (response.status === 200) {
            localStorage.setItem('access_token', response.data.access);
            localStorage.setItem('user', JSON.stringify(response.data.user));
        }
      });
  };

ユーザー情報更新画面(ログインが必要な画面)を作成

getUserではHeaderにAuthorizationを設定します。
DjangoではこのAuthorizationをもとに認証します。

UpdateForm.jsx
const getUser = (data) => {
    const config = {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
        }
    };
    const userJson = JSON.parse(localStorage.getItem('user'));
    const url = `${API_URL}/api/users/${userJson.pk}`;
    return axios.get(url, config)
};

export const UpdateForm = () => {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    password: '',
    bio: '',
    location: '',
    birthYear: '',
    birthMonth: '',
    birthDay: '',
  });

  // ユーザーデータを取得してフォームに設定
  useEffect(() => {
    const loadUserData = async () => {
      try {
        const response = await getUser(); // ユーザー情報取得API
        const birthDate = new Date(response.data.birth_date);
        setFormData({
          ...formData,
          firstName: response.data.first_name,
          lastName: response.data.last_name,
          email: response.data.email,
          password: '', // パスワードは通常非表示
          bio: response.data.bio,
          location: response.data.location,
          birthYear: birthDate.getFullYear().toString(),
          birthMonth: (birthDate.getMonth() + 1).toString(), // JavaScriptの月は0から始まるため+1
          birthDay: birthDate.getDate().toString(),
        });
      } catch (error) {
        console.error('Failed to fetch user data:', error);
      }
    };
    loadUserData();
  }, []);
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0