はじめに
個人開発で、DRFとReactで認証処理を作成しました。
普段は日記がてら投稿しているのですが、ある程度まとめたものを投稿しようと思います。
認証関連の記事
インストール
dj-rest-authとdjangorestframework-simplejwtをインストール。
dj-rest-authは簡単に認証処理をカスタマイズできるライブラリで、
djangorestframework-simplejwtは、簡単にJWT認証を実装できるライブラリです。
pip install dj-rest-auth djangorestframework-simplejwt
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を作成
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を作成
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
メソッドをカスタマイズしています。
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にかかわる部分のみを書きます。
ユーザー登録画面を作成
ユーザー登録後にアクセストークンをローカルストレージに保存します。
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));
}
});
};
ログイン画面を作成
こちらもログイン完了後にアクセストークンをローカルストレージに保存します。
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
をもとに認証します。
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();
}, []);