Reactjs + Django REST Framework ( DRF ) 環境を構築しながら、メール認証方法を適用したユーザ登録機能を実装します。Django ではデフォルトで username/email/password パターンでユーザを登録する仕組みなので、Email/password で登録 ( Log in / Sign up ) できるように Custom User Model を適用します。
現状・条件
- バックエンド環境:Django / Django Rest Framework / Django-Rest-Auth
- フロントエンド環境:React / Redux / Bootstrap
- OS 環境 : MacBook Pro / macOS High Sierra
- Python 3.7.2
- PostgreSQL 11.3
バックエンド環境を構築
Django 環境を構築
email-auth ディレクトリを作成して作業を行います。( ディレクトリ名・場所は任意 )
バックエンドは最終的に下記 ( ▼ ) のような構造になります。
(backend) try🐶everything backend$ tree -d
.
├── project << Django プロジェクト名
├── tmp
│ └── emails << ユーザ認証用メールを保存
├── user_profile << Django アプリ名
│ └── migrations
└── users << Django アプリ名
└── migrations
7 directories
(backend) try🐶everything backend$
try🐶everything ~$ mkdir email-auth try🐶everything ~$ cd email-auth/ try🐶everything email-auth$
別のツールを使用しても構いませんが、ここでは Python 仮想環境やパッケージを管理するため、pipenv を使用しますので、下記のコマンドでインストールしておきます。
try🐶everything email-auth$ brew install pipenv
バックエンド用ディレクトリ backend を生成します。
try🐶everything email-auth$ mkdir backend try🐶everything email-auth$ cd backend/
そこに仮想環境を作成します。
コマンド:pipenv install
try🐶everything backend$ pipenv install Creating a virtualenv for this project… Pipfile: /Users/macadmin/email-auth/backend/Pipfile Using /usr/local/bin/python3 (3.7.2) to create virtualenv… ⠦ Creating virtual environment...Using base prefix '/usr/local/Cellar/python/3.7.2_2/Frameworks/Python.framework/Versions/3.7' New python executable in /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/python3.7 Also creating executable in /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/python Installing setuptools, pip, wheel... done. Running virtualenv with interpreter /usr/local/bin/python3 ✔ Successfully created virtual environment! Virtualenv location: /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek Installing dependencies from Pipfile.lock (03de17)… 🐍 ▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉ 15/15 — 00:00:16 To activate this project's virtualenv, run pipenv shell. Alternatively, run a command inside the virtualenv with pipenv run. try🐶everything backend$
仮想環境に入ります。
コマンド:pipenv shell ( 出る:exit )
try🐶everything backend$ pipenv shell Launching subshell in virtual environment… try🐶everything backend$ . /Users/macadmin/.local/share/virtualenvs/backend-Fs_44zek/bin/activate (backend) try🐶everything backend$
仮想環境に必要なパッケージをインストールします。
2019/10/07 時点での、Python パッケージリストです。
ダウンロード後、pip install -r … コマンドでインストールします。
(backend) try🐶everything backend$ pip install -r ~/Downloads/requirements.txt
それでは、Django 環境を設定するため、新しいプロジェクトを生成し、settings.py と urls.py を編集します。
新しいプロジェクトを生成します。
プロジェクト名は任意ですが、ここでは project にします。
プロジェクトを生成後、project/settings.py を編集します。
(backend) try🐶everything backend$ django-admin startproject project . (backend) try🐶everything backend$ cd project (backend) try🐶everything project$ vi settings.py
- 1INSTALLED_APPS
ユーザ認証や API 機能を使用するためのパッケージを有効化します。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.sites', 'rest_framework', 'rest_framework.authtoken', 'rest_auth', 'rest_auth.registration', # Third-party 'allauth', 'allauth.account', 'allauth.socialaccount', # 'allauth.socialaccount.providers.facebook', # 'allauth.socialaccount.providers.twitter', # rest cors support 'corsheaders', # Local 'users', 'user_profile', ] - 2MIDDLEWARE
MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'corsheaders.middleware.CorsMiddleware', ] - 3TEMPLATES
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] - 4DATABASES:PostgreSQLを使用する場合
# DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } # } DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'データベース名', 'USER': 'ユーザ名', 'PASSWORD': 'パスワード', 'HOST': 'localhost', 'PORT': '', } } - 5TIME_ZONE / SITE_ID
TIME_ZONE = 'Asia/Tokyo' SITE_ID = 1
- 6AUTHENTICATION_BACKENDS
ユーザー名の代わりに電子メールでの登録を可能にするために設定します。
AUTHENTICATION_BACKENDS = ( "django.contrib.auth.backends.ModelBackend", "allauth.account.auth_backends.AuthenticationBackend", ) - 7django-allauth
AUTH_USER_MODEL = 'users.CustomUser' ACCOUNT_ADAPTER = 'user_profile.adapter.MyAccountAdapter' ACCOUNT_USERNAME_REQUIRED = False ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' ACCOUNT_UNIQUE_EMAIL = True ACCOUNT_USER_MODEL_EMAIL_FIELD = 'email'
- 8django-allauth: account
PASSWORD_RESET_TIMEOUT_DAYS = 1 ACCOUNT_AUTHENTICATION_METHOD = 'email' ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 3 ACCOUNT_EMAIL_SUBJECT_PREFIX = '' ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5 ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = False # to keep the user logged in after password change
- 9django-rest-auth:REST_AUTH_SERIALIZERS
REST_AUTH_SERIALIZERS = { 'USER_DETAILS_SERIALIZER': 'users.serializers.UserProfileSerializer' } REST_SESSION_LOGIN = False OLD_PASSWORD_FIELD_ENABLED = False LOGOUT_ON_PASSWORD_CHANGE = False - 10REST_FRAMEWORK
REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', #'rest_framework.permissions.AllowAny', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), 'DEFAULT_THROTTLE_CLASSES': ( 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle' ), 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day' }, 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 100, 'DEFAULT_PARSER_CLASSES': ( 'rest_framework.parsers.JSONParser', 'rest_framework.parsers.FormParser', 'rest_framework.parsers.MultiPartParser' ), } - 11CORS Setting
# Change CORS settings as needed # CORS_ORIGIN_ALLOW_ALL = True CORS_ORIGIN_ALLOW_ALL = False # https://github.com/ottoyiu/django-cors-headers # CORS_ORIGIN_WHITELIST = ( # 'localhost:3000', # '127.0.0.1:3000', # '192.168.11.7:3000', # ) # Or, CORS_ORIGIN_REGEX_WHITELIST = ( r'^(http?://)?localhost', r'^(http?://)?127.', r'^(http?://)?192.168.11.', ) - 12Email Auth Settings
開発中には、tmp/emails 下に認証用メール本文が届きます。
本番なら、[SMTP] Email Settings をご使用ください。#------------------------- # [Dev] Email Settings #------------------------- EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_FILE_PATH = 'tmp/emails' DEFAULT_FROM_EMAIL = 'admin@example.com' #------------------------- # [SMTP] Email Settings #------------------------- # # EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # default # DEFAULT_FROM_EMAIL = 'admin@example.com' # EMAIL_HOST = 'smtp.example.com' # EMAIL_HOST_USER = 'admin@example.com' # EMAIL_HOST_PASSWORD = 'your_password' # EMAIL_PORT = 465 # smtps # EMAIL_USE_SSL = True
次は、project/urls.py を編集します。
(backend) try🐶everything project$ vi urls.py
- 1router / urlpatterns
from django.urls import re_path, include from django.contrib import admin from django.views.generic import TemplateView from rest_framework import routers from user_profile.views import UserViewSet router = routers.DefaultRouter() router.register(r'user', UserViewSet,) # ignored users.urls 'user' path urlpatterns = [ re_path(r'^api/', include(router.urls)), re_path(r'^api/', include('users.urls')), # Authenticate API Site re_path(r'^api-auth/', include('rest_framework.urls')), # This is used for user reset password re_path(r'^', include('django.contrib.auth.urls')), re_path(r'^rest-auth/', include('rest_auth.urls')), re_path(r'^rest-auth/registration/', include('rest_auth.registration.urls')), re_path(r'^account/', include('allauth.urls')), re_path(r'^admin/', admin.site.urls), ]この設定によって、DRF サイトのアクセス URL は localhost:8000 ではなく、localhost:8000/api/ から閲覧出来るようになります。
次は、ユーザー名の代わりに電子メールでの登録を可能にするための設定を行います。
Email / Password でログインできるように変更します。
※ 以下は djangoX でインサイトを得たものです。
Django アプリ users を作成します。( アプリ名は任意 )
(backend) try🐶everything backend$ django-admin startapp users (backend) try🐶everything backend$ cd users
users/admin.py を編集します。
from django.contrib import admin from .models import CustomUser admin.site.register(CustomUser)
users/models.py を編集します。
from django.db import models
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.utils.translation import ugettext_lazy as _
class MyUserManager(BaseUserManager):
"""
A custom user manager to work with emails instead of usernames
"""
def create_user(self, email, password, **extra_fields):
"""
Creates and saves a User with the given email and password.
"""
if not email:
raise ValueError('The Email must be set')
email = self.normalize_email(email)
user = self.model(email=email, is_active=True, **extra_fields)
user.set_password(password)
user.save()
return user
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_superuser', True)
extra_fields.setdefault('is_staff', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self.create_user(email, password, **extra_fields)
def search(self, kwargs):
qs = self.get_queryset()
print(kwargs)
if kwargs.get('first_name', ''):
qs = qs.filter(first_name__icontains=kwargs['first_name'])
if kwargs.get('last_name', ''):
qs = qs.filter(last_name__icontains=kwargs['last_name'])
if kwargs.get('department', ''):
qs = qs.filter(department__name=kwargs['department'])
if kwargs.get('company', ''):
qs = qs.filter(company__name=kwargs['company'])
return qs
class CustomUser(AbstractBaseUser, PermissionsMixin):
"""
Customized User model itself
"""
username = models.CharField(_('username'), max_length=150, blank=True)
email = models.EmailField(unique=True, null=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
first_name = models.CharField(default='', max_length=60, blank=True)
last_name = models.CharField(default='', max_length=60, blank=True)
current_position = models.CharField(default='', max_length=64, blank=True)
about = models.CharField(default='', max_length=255, blank=True)
department = models.CharField(default='', max_length=128, blank=True)
company = models.CharField(default='', max_length=128, blank=True)
date_joined = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = 'email'
objects = MyUserManager()
def __str__(self):
return self.email
def get_full_name(self):
return self.email
def get_short_name(self):
return self.email
users/serializers.py を作成します。( 新 )
import hashlib
from rest_framework import serializers
from rest_framework.validators import UniqueValidator
from .models import CustomUser
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
for field_name in existing - allowed:
self.fields.pop(field_name)
class UserItemSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = ('id', 'email', 'first_name', 'last_name', 'date_joined')
class UserProfileSerializer(serializers.ModelSerializer):
"""
Class to serialize data for user profile details
"""
class Meta:
model = CustomUser
fields = ('id', 'email', 'username', 'first_name', 'last_name',
'current_position', 'about', 'company', 'department', 'date_joined')
class UserSerializer(serializers.ModelSerializer):
"""
Class to serialize data for user validation
"""
first_name = serializers.CharField(max_length=60)
last_name = serializers.CharField(max_length=60)
current_position = serializers.CharField(max_length=64)
about = serializers.CharField(max_length=255)
class Meta:
model = CustomUser
fields = ('id', 'email','username', 'first_name', 'last_name',
'current_position', 'about', 'company', 'department','date_joined')
def validate(self, data):
if len(data['first_name']) + len(data['last_name']) > 60:
raise serializers.ValidationError({
'first_name': 'First + Last name should not exceed 60 chars'})
return data
class UserRegistrationSerializer(serializers.Serializer):
"""
Serializer for Registration - password match check
"""
email = serializers.EmailField(required=True, validators=[
UniqueValidator(queryset=CustomUser.objects.all())])
password = serializers.CharField()
confirm_password = serializers.CharField()
def create(self, data):
return CustomUser.objects.create_user(data['email'], data['password'])
def validate(self, data):
if not data.get('password') or not data.get('confirm_password'):
raise serializers.ValidationError({
'password': 'Please enter password and confirmation'})
if data.get('password') != data.get('confirm_password'):
raise serializers.ValidationError(
{'password': 'Passwords don\'t match'})
return data
users/urls.py を作成します。( 新 )
from django.urls import re_path from users import views urlpatterns = [ re_path(r'^me/$', views.CurrentUser.as_view()), re_path(r'^register/$', views.Registration.as_view()), re_path(r'^users/$', views.UserList.as_view()), re_path(r'^user/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()), re_path(r'^search/$', views.UserSearch.as_view()), ]
users/views.py を編集します。
from django.http import Http404
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, IsAdminUser, AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import status, generics
from .serializers import (
UserProfileSerializer,
UserItemSerializer,
UserRegistrationSerializer,
UserSerializer,
)
from .models import CustomUser
def index(request):
return render(request, 'index.html')
class UserList(APIView):
"""
List all users
"""
permission_classes = (IsAuthenticated, )
def get(self, request, format=None):
users = CustomUser.objects.all()
serializer = UserItemSerializer(users, many=True)
return Response({'users': serializer.data})
class UserSearch(APIView):
"""
Advanced user search
"""
permission_classes = (IsAuthenticated, )
def post(self, request):
if request.data:
users = CustomUser.objects.search(request.data)
else:
users = CustomUser.objects.all()
serializer = UserProfileSerializer(users, many=True)
return Response({'users': serializer.data})
class UserDetail(APIView):
"""
User profile details
"""
permission_classes = (IsAuthenticated, )
def get_object(self, pk):
try:
return CustomUser.objects.get(pk=pk)
except Todo.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
user = self.get_object(pk)
serializer = UserProfileSerializer(user)
return Response(serializer.data)
def put(self, request, pk, format=None):
user = self.get_object(pk)
if request.user != user:
return Response(status=status.HTTP_403_FORBIDDEN)
serializer = UserSerializer(user, request.data)
if serializer.is_valid():
serializer.save()
return Response({'success': True})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class CurrentUser(APIView):
permission_classes = (IsAuthenticated, )
def get(self, request, *args, **kwargs):
if request.user.id == None:
raise Http404
serializer = UserProfileSerializer(request.user)
data = serializer.data
data['is_admin'] = request.user.is_superuser
return Response(data)
class Registration(generics.GenericAPIView):
serializer_class = UserRegistrationSerializer
permission_classes = (AllowAny,)
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = self.perform_create(serializer, request.data)
return Response({}, status=status.HTTP_201_CREATED)
def perform_create(self, serializer, data):
user = serializer.create(data)
return user
[ad:


コメント