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:
コメント