前回までの記事
1.Django REST framework + Vue.js「SEIYU風のECサイトを作りましょう」全6回(予定)
2.SEIYU風のECサイトを作りましょう(1)要求分析とプロジェクト初期化
3.SEIYU風のECサイトを作りましょう(2)Xadminを使って管理画面を一新します
4.SEIYU風のECサイトを作りましょう(3)Django REST frameworkで爆速APIを作りましょう
5.SEIYU風のECサイトを作りましょう(4) Vue.js プロジェクト初期化 TypeScript使用
今までの内容観てなくとも今回の内容わかりますので、最後までお付き合いして頂けると幸いです。
JWTとは何か
JSON Web Tokenの略称になります、トークン内に任意の情報(クレーム)を保持することが可能であり、例えばサーバーはクライアントに対して「管理者としてログイン済」という情報を含んだトークンを生成することができる。クライアントはそのトークンを、自身が管理者としてログイン済であることの証明に使用することができる。 --[Wikipedia]
(https://ja.wikipedia.org/wiki/JSON_Web_Token)
手書きで実装
django-rest-framework-jwtというライブラリ使用しても、簡単にJWTログイン実装可能ですが、
直接書いた方が色々とカスタマイズしやすいので、直接書きます。
*使い方知りたい方がいれば、また追記いたします。
新規でDjango REST frameworkのプロジェクトを作ります
pip install Django
pip install djangorestframework
pip install markdown
pip install django-filter
django-admin startproject jwttest
cd jwttest
python manage.py runserver
サーバー起動して、正常にロケット見れたらOKです。
初期設定
新規appを作ります。
python manage.py startapp api
rest_framework
と一緒に INSTALLED_APPS
に追加します。
INSTALLED_APPS = [
...
'rest_framework',
'api'
]
ログイン用のユーザーモデルを作ります。
from django.db import models
class UserInfo(models.Model):
username = models.CharField(max_length=50, unique=True, db_index=True)
password = models.CharField(max_length=100, db_index=True)
info = models.CharField(max_length=200)
DBマイグレーションを実行します。
python manage.py makemigrations
python manage.py migrate
ログイン機能を実装します
JWTtoken生成用のライブラリをインストールします。
pip install pyjwt
ログイン用のclassを追加します、
api
ディレクトリの配下にutils
フォルダを追加し、中にauth.py
ファイルを新規追加します。
import time
import jwt
from jwttest.settings import SECRET_KEY
from rest_framework.authentication import BaseAuthentication
from rest_framework import exceptions
from api.models import UserInfo
class NormalAuthentication(BaseAuthentication):
def authenticate(self, request):
username = request._request.POST.get("username")
password = request._request.POST.get("password")
user_obj = UserInfo.objects.filter(username=username).first()
if not user_obj:
raise exceptions.AuthenticationFailed('認証失敗')
elif user_obj.password != password:
raise exceptions.AuthenticationFailed('パスワードあってません')
token = generate_jwt(user_obj)
return (token, None)
def authenticate_header(self, request):
pass
# 先程インストールしたjwtライブラリでTokenを生成します
# Tokenの内容はユーザーの情報とタイムアウトが含まれてます
# タイムアウトのキーはexpであることは固定してます
# ドキュメント: https://pyjwt.readthedocs.io/en/latest/usage.html?highlight=exp
def generate_jwt(user):
timestamp = int(time.time()) + 60*60*24*7
return jwt.encode(
{"userid": user.pk, "username": user.username, "info": user.info, "exp": timestamp},
SECRET_KEY).decode("utf-8")
ログイン用のviewも追加します。
もしログイン成功したら、JWTを返す仕組みです。
authentication_classes
に先程作ったNormalAuthentication
を追加します。
from rest_framework.views import APIView
from rest_framework.response import Response
from .utils.auth import NormalAuthentication
class Login(APIView):
authentication_classes = [NormalAuthentication,]
def post(self, request, *args, **kwargs):
return Response({"token": request.user})
urlを追加します。
...
from api.views import Login
urlpatterns = [
...
path('login/', Login.as_view()),
]
サーバー立ち上げて、実際ログインしてみます。
返されたJWTをhttps://jwt.io/
で解析してみます。
JWTにちゃんと指定した情報が含まれています。
ログインしてないと見れないviewを作って、このToken使ってアクセスしてみよう。
まずapi/utils/auth.py
にJWT用の認証用classを追加します。
...
class JWTAuthentication(BaseAuthentication):
keyword = 'JWT'
model = None
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != self.keyword.lower().encode():
return None
if len(auth) == 1:
msg = "Authorization 無効"
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = "Authorization 無効 スペースはない"
raise exceptions.AuthenticationFailed(msg)
try:
jwt_token = auth[1]
jwt_info = jwt.decode(jwt_token, SECRET_KEY)
userid = jwt_info.get("userid")
try:
user = UserInfo.objects.get(pk=userid)
user.is_authenticated = True
return (user, jwt_token)
except:
msg = "ユーザー存在しません"
raise exceptions.AuthenticationFailed(msg)
except jwt.ExpiredSignatureError:
msg = "tokenはタイムアウトしました"
raise exceptions.AuthenticationFailed(msg)
def authenticate_header(self, request):
pass
...
ログインしてないとアクセスできないviewをapi/views.py
に追加。
...
from rest_framework.permissions import IsAuthenticated
...
class Something(APIView):
authentication_classes = [JWTAuthentication, ]
# ログインしてるユーザーだけアクセスできるようにします。
permission_classes = [IsAuthenticated, ]
def get(self, request, *args, **kwargs):
return Response({"data": "中身です"})
...
urlを足したらアクセスしてみます。
path('data/', Something.as_view())
まずはTokenない状態でアクセスします。
認証資格情報が提供されませんでした
と返ってきました。
以上で終わりですが、Django REST frameworkのログイン関連のソースコードの分析も多少いれたいと思います。
興味ある方は読んでください。
DRFのログインソースコード分析
使用する認証classとのリンク
先程書いたコードを元に分析して行きます、
CBV(Class-based views)使用する場合、dispatch
は実行されます。
それを入り口として、ソースコード見ていきます。
見方としては、先程書いたログイン用のclassにself.dispatch()
を追加します。
...
class Login(APIView):
authentication_classes = [NormalAuthentication, ]
def post(self, request, *args, **kwargs):
# 追加して、ソースコード辿っていきます
# PyCharm使用する場合
# macではcommand+クリック
# winではAlt+クリックのはず
self.dispatch()
return Response({"token": request.user})
...
行き先はrest_framework/views.py
の481行のdef dispatch(self, request, *args, **kwargs):
になります。
488行目のinitialize_request
関数の中身をみていきます。
def dispatch(self, request, *args, **kwargs):
...
# この関数の中身を見てきます
request = self.initialize_request(request, *args, **kwargs)
self.request = request
中身の定義はrest_framework/views.py
の381行目にあります。
def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
return Request(
request,
parsers=self.get_parsers(),
# ここ
authenticators=self.get_authenticators(),
negotiator=self.get_content_negotiator(),
parser_context=parser_context
)
そして390行にあるself.get_authenticators()
の中身を見にいきますと、
以下のコードがあります、該当CBV使用してる認証classは何なのか、self.authentication_classes
から取ってきてます。
def get_authenticators(self):
"""
Instantiates and returns the list of authenticators that this view can use.
"""
return [auth() for auth in self.authentication_classes]
self.authentication_classes
の定義を追っていきますと、
rest_framework/views.py
の109行目では、以下の定義があります。
class APIView(View):
...
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
よって、認証class何を使用するのか、直接定義出来る箇所は二つありまして、
-
APIView
継承したCBV
classの内部
...
class Login(APIView):
# ここ
authentication_classes = [NormalAuthentication, ]
def post(self, request, *args, **kwargs):
return Response({"token": request.user})
...
2.settings.py
のREST_FRAMEWORK
用設定の内部
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['api.utils.auth.NormalAuthentication']
}
CBVと認証用classの関連性を把握したら、次は認証用classの機能を見ていきます。
またdispatch
から辿っていきます、rest_framework/views.py
の493行のself.initial(request, *args, **kwargs)
def dispatch(self, request, *args, **kwargs):
...
try:
self.initial(request, *args, **kwargs)
...
それの定義を追っていきます。rest_framework/views.py
の395行目。
perform_authentication
があります、さらに辿っていきます。
...
def initial(self, request, *args, **kwargs):
...
self.perform_authentication(request)
...
その先にrequest.user
があります。
...
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.
Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
request.user
...
それの定義は rest_framework/request.py
の213行にあります。
...
@property
def user(self):
"""
Returns the user associated with the current request, as authenticated
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
with wrap_attributeerrors():
self._authenticate()
return self._user
...
self._authenticate()
の定義を見てみます。 rest_framework/request.py
の366行目。
内容としては、CBVの認証classリストから、classを取り出して、そのclassのauthenticate
メソッドを実行します。
実行した結果は二つの要素が入ってるtuple
である、tupleの一番目はself.user
になる、二番目はself.auth
になります。
def _authenticate(self):
"""
Attempt to authenticate the request using each authentication instance
in turn.
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if user_auth_tuple is not None:
self._authenticator = authenticator
self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
それを踏まえて、先程定義したJWT用の認証classを見てみます。
class JWTAuthentication(BaseAuthentication):
keyword = 'JWT'
model = None
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != self.keyword.lower().encode():
return None
if len(auth) == 1:
msg = "Authorization 無効"
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = "Authorization 無効 スペースはない"
raise exceptions.AuthenticationFailed(msg)
try:
jwt_token = auth[1]
jwt_info = jwt.decode(jwt_token, SECRET_KEY)
userid = jwt_info.get("userid")
try:
user = UserInfo.objects.get(pk=userid)
user.is_authenticated = True
return (user, jwt_token)
except:
msg = "ユーザー存在しません"
raise exceptions.AuthenticationFailed(msg)
except jwt.ExpiredSignatureError:
msg = "tokenはtimeout"
raise exceptions.AuthenticationFailed(msg)
def authenticate_header(self, request):
pass
classにauthenticate
メソッド含まれています。
認証成功すれば、ユーザー情報含むtupleを返します。
ログイン関連ソースの解析は以上となります。
最後まで付き合って頂きありがとうございます。