今回やること
前にJWT認証の記事を上げたのですがバックエンドの説明が皆無だったのでこれから複数回に分けてあげていきます(単に暇なだけというのもある)。使う言語はGoとPythonで迷ったのですが、Gin(Goの言語のAPIフレームワーク)は日本語の文献がほぼ皆無なので需要がありそうなPythonのDjangoを使います。今回からはサンプルとして簡単な記事を投稿できるシステムを作ります。
APIの設計
APIとはアプリケーションプログラミングインターフェースのことで、特にこの記事ではクライアント(Webブラウザなど)からのHTTPリクエストに応じてデータベースなどとやり取りして特定のデータフォーマット(JSONなど)のレスポンスを返すシステムを指すと思ってください。
今回のAPIエンドポイントの設計はこんな感じ。
path | method | params | detail |
---|---|---|---|
/api/users | GET, POST | null | false |
/api/user/:id | GET, PATCH, PUT, DELETE | id: int | true |
/api/posts | GET, POST | null | false |
/api/posts/:slug | GET, PATCH, PUT, DELETE | slug: string | true |
/api/token | POST | null | true |
/api/token/refresh | POST | null | true |
/api/token/logout | POST | null | true |
slugとはurlによくくっついてるやつです(hello-worldみたいな)。5つ目からはJWTにかかわる話なのでここでは割愛します。
今回必要なもの
まず今回必須になるものから説明します。
- Python 3
- Django
- Django rest framework
- pip
- virtualenv
virtualenvはpythonの仮想環境を作成+管理するためのパッケージです。このおかげでバージョンの管理などで悩まなくて済みます。作業をする時は必ず仮想環境に入るようにしてください。
pipはpythonのパッケージ管理ソフトウェアです。Anacondaをインストールされている方はすでに入っていると思います。
必須でないものまでまとめるとこんな感じ。
appdirs==1.4.4
asgiref==3.2.10
black==21.6b0
certifi==2020.12.5
cffi==1.14.5
chardet==4.0.0
click==8.0.1
colorama==0.4.4
coreapi==2.3.3
coreschema==0.0.4
cryptography==3.4.7
defusedxml==0.7.1
Django==3.1
django-cors-headers==3.7.0
django-filter==2.4.0
django-templated-mail==1.1.1
django-versatileimagefield==2.0
djangorestframework==3.12.4
djangorestframework-jwt==1.11.0
djangorestframework-simplejwt==4.6.0
idna==2.10
itypes==1.2.0
Jinja2==3.0.0
jwt==1.2.0
MarkupSafe==2.0.0
mypy-extensions==0.4.3
oauthlib==3.1.0
pathspec==0.8.1
Pillow==8.2.0
pycparser==2.20
PyJWT==2.1.0
python-magic-bin==0.4.14
python-slugify==5.0.2
python3-openid==3.2.0
pytz==2021.1
requests==2.25.1
requests-oauthlib==1.3.0
six==1.16.0
social-auth-app-django==4.0.0
social-auth-core==4.1.0
sqlparse==0.4.1
text-unidecode==1.3
toml==0.10.2
uritemplate==3.0.1
urllib3==1.26.4
インストールするときはまとめて一つのファイルに記しておくと同時にインストールできます。コマンドは後程。
インストールと環境構築
まず仮想環境を作成します。virtualenvは以下のコマンドでインストールできます。
pip install virtualenv
// pathが通っていない方はこちら
python -m pip install virtualenv
次に仮想環境を作成します。
virtualenv myenv
これでmyenvという仮想環境ができました(pythonのバージョンも指定できます)。
仮想環境を起動するには、myenvディレクトリに入って次のコマンドを打ってください。
D:/myenv> ../Scripts/activate.bat
(myenv) D:/myenv>
こんな感じで(name)みたいなのが出てくるのでこれが印です。出るときは同じ場所でdeactivateと打ってください。
ではここから先ほど紹介したものをインストールします。同ディレクトリに先ほどのrequirements.txtファイルを作成しインストールします。
(myenv) D:/myenv> pip install -r requirements.txt
たまに信じられないほどインストールに時間がかかりますが仏の心で待ちましょう。
これですべてインストールできたので、いよいよプロジェクトを起動します。
(myenv) D:/myenv> django-admin startproject backend
// backendのところはプロジェクト名
フォルダ移動。そしてアプリを登録。
(myenv) D:/myenv> cd backend
(myenv) D:/myenv/backend> python manage.py startapp accounts
(myenv) D:/myenv/backend> python manage.py startapp posts
これでpostsとaccountsというフォルダがbackendディレクトリに作成されます。構成はこんな感じ。
backend
├─.pytest_cache
│ └─v
│ └─cache
├─.vscode
├─accounts
│ ├─migrations
│ │ └─__pycache__
│ └─__pycache__
├─backend
│ └─__pycache__
├─posts
│ ├─migrations
│ │ └─__pycache__
│ └─__pycache__
└─utils
└─__pycache__
これから話すのはVScodeを使っている人向けです。.vscodeフォルダ(上に書いてあるやつ)にlaunch.jsonというファイルを作成し以下を入力すると便利です。
{
"version": "0.2.0",
"configurations": [
{
"name": "run server",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}\\manage.py",
"args": ["runserver"],
"console": "externalTerminal",
"stopOnEntry": false,
},
{
"name": "make migration files",
"request": "launch",
"cwd": "${workspaceFolder}",
"type": "python",
"program": "${workspaceFolder}\\manage.py",
"args": ["makemigrations"],
"console": "externalTerminal"
},
{
"name": "migrate",
"type": "python",
"request": "launch",
"cwd": "${workspaceFolder}",
"program": "${workspaceFolder}\\manage.py",
"args": ["migrate"],
"console": "externalTerminal"
}
]
}
これで毎回コマンド入力しなくてもF5で実行できます。
プロジェクトの設定
backendフォルダ内にsettings.pyというファイルがあるのでそこに先ほど追加したアプリを登録します。
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # 追加
'corsheaders', #追加
'posts', # 追加
'accounts', #追加
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
これから新しいアプリを登録するときには必ず同じ操作をしてください
言語の設定もしておきます
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
初回からいきなりハードですが、カスタムユーザーモデルを作ります。後で変更しようとして泥沼にはまったことがあるので先に作っておきます。デフォルトから大きく変更する必要はありません。ただ作っておいたほうがかなり便利です。認証にはemailとpasswordを使います。
accountsフォルダのmodels.pyファイルに以下を入力します。
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
class MyAccountManager(BaseUserManager):
def create_user(self, email, username, password=None, **extra_kwargs):
if not email:
raise ValueError('Users must have an email address')
if not username:
raise ValueError('Users must have an username')
user = self.model(
email = self.normalize_email(email),
username = username,
**extra_kwargs
)
user.set_password(password)
user.save(using = self._db)
return user
def create_superuser(self, email, username, password):
user = self.create_user(
email = self.normalize_email(email),
username = username,
password = password
)
user.is_admin = True
user.is_staff = True
user.is_superuser = True
user.save(using=self._db)
return user
class User(AbstractBaseUser):
email = models.EmailField(verbose_name="email", max_length=60, unique=True)
username = models.CharField(max_length=30, unique=True)
date_joined = models.DateTimeField(
verbose_name='date joined', auto_now_add=True)
last_login = models.DateTimeField(verbose_name='last login', auto_now=True)
is_admin = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
objects = MyAccountManager()
def __str__(self):
return self.username
def get_absolute_url(self):
return '/users/{}'.format(self.id)
# 絶対書くこと(必須)
def has_perm(self, perm, obj=None):
return self.is_admin
# これも絶対書くこと(必須)
def has_module_perms(self, app_label):
return True
class Meta:
ordering = ['-date_joined']
何が起こっているのか知りたくなる気持ちはわかりますがこれは水面下でかなり複雑なことが起こっているのでできればコピペしてください。
さらにこれよりもカスタマイズする方法は次回やります。
このモデルをプロジェクト設定に登録します。
from pathlib import Path
import os
from corsheaders.defaults import default_headers
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
CORS_ALLOWED_ORIGINS = [
# frontend側の設定
]
CORS_ALLOW_HEADERS = default_headers + ('contenttype', 'Authorization')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'posts',
'accounts',
'rest_framework_simplejwt.token_blacklist', #JWT認証を使わないなら要らない
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'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',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME':
'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME':
'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/
LANGUAGE_CODE = 'ja'
TIME_ZONE = 'Asia/Tokyo'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/
STATIC_URL = '/static/'
# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
AUTH_USER_MODEL = 'accounts.User' #この部分を新たに追加
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.AllowAny', ),
'DEFAULT_AUTHENTICATION_CLASSES':
('rest_framework_simplejwt.authentication.JWTAuthentication', ),
}
次に管理サイトにこのモデルを追加します。同ディレクトリにあるadmin.pyファイルに移動し以下を入力します。
from django.contrib import admin
from .models import User
admin.site.register(User)
最後にマイグレーションしてサーバーを起動してみてください。
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
管理サイトが開くはずです。
おまけ
utilsなんてフォルダないぞ?と思ったあなた、よく見てますね。これは僕が勝手に作ったもので、ユーザーの権限とかの制限を管理するスクリプトを入れてます。あると便利なのでよかったらどうぞ。
from rest_framework import permissions
class IsAdminOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_staff
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.posts.author == request.user
class IsSelfOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj == request.user
from rest_framework_simplejwt.authentication import JWTAuthentication
from rest_framework_simplejwt.exceptions import InvalidToken
class JWTAuthenticationSafe(JWTAuthentication):
def authenticate(self, request):
try:
return super().authenticate(request=request)
except InvalidToken:
return None
最後に
次回はいろいろやります。
お疲れさまでした。