Django REST framework に JWTログイン機能を実装する

# 前回までの記事

1.[Django REST framework + Vue.js「SEIYU風のECサイトを作りましょう」全6回（予定）](https://qiita.com/Syoitu/items/62e6ec1437d98d6b9d28)
2.[SEIYU風のECサイトを作りましょう（1）要求分析とプロジェクト初期化](https://qiita.com/Syoitu/items/eae447432c6135ff1174)
3.[SEIYU風のECサイトを作りましょう（2）Xadminを使って管理画面を一新します](https://qiita.com/Syoitu/items/11fac037759220b30cd2)
4.[SEIYU風のECサイトを作りましょう（3）Django REST frameworkで爆速APIを作りましょう](https://qiita.com/Syoitu/items/2ab045f5618bf9cb31d4)
5.[SEIYU風のECサイトを作りましょう(4) Vue.js プロジェクト初期化 TypeScript使用](https://qiita.com/Syoitu/items/324a40a954324fab4e96)

今までの内容観てなくとも今回の内容わかりますので、最後までお付き合いして頂けると幸いです。:relaxed:

## JWTとは何か

JSON Web Tokenの略称になります、トークン内に任意の情報（クレーム）を保持することが可能であり、例えばサーバーはクライアントに対して「管理者としてログイン済」という情報を含んだトークンを生成することができる。クライアントはそのトークンを、自身が管理者としてログイン済であることの証明に使用することができる。  --[Wikipedia]
(https://ja.wikipedia.org/wiki/JSON_Web_Token)

# 手書きで実装

[django-rest-framework-jwt](https://github.com/jpadilla/django-rest-framework-jwt)というライブラリ使用しても、簡単にJWTログイン実装可能ですが、
直接書いた方が色々とカスタマイズしやすいので、直接書きます。
＊使い方知りたい方がいれば、また追記いたします。:point_up_tone1:

### 新規で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`に追加します。

```python:jwttest/settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'api'
]
```
ログイン用のユーザーモデルを作ります。

```python:jwtest/api/models.py
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`ファイルを新規追加します。

```python: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 `を追加します。

```python:api/views.py
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を追加します。

```python:jwttest/urls.py
...
from api.views import Login

urlpatterns = [
    ...
    path('login/', Login.as_view()),
]
```
ユーザー情報一つ追加して、![B2DC6C55-949F-4FB9-ADCB-ED3C0F894B1E_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/9608aed2-dfb2-7b90-bce8-5bf6bc953926.jpeg)

サーバー立ち上げて、実際ログインしてみます。
![82BFEFD1-D012-4F3A-80D8-5CC828590481.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/967b21da-ed1f-ee16-deee-d27a45460e32.jpeg)
返されたJWTを`https://jwt.io/`で解析してみます。
![BE755CEB-725D-4833-A954-0300EF1A4E0B.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/dce27067-419b-bcb6-a001-f80e1e239c3c.jpeg)
JWTにちゃんと指定した情報が含まれています。
ログインしてないと見れないviewを作って、このToken使ってアクセスしてみよう。
まず`api/utils/auth.py`にJWT用の認証用classを追加します。

```python:api/utils/auth.py
...
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`に追加。

```python: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を足したらアクセスしてみます。

```python:jwttest:urls.py
    path('data/', Something.as_view())
```
まずはTokenない状態でアクセスします。
`認証資格情報が提供されませんでした`と返ってきました。
![FFA8B8F1-6250-40FF-9FA9-6DDEB186B3EB_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/0b631466-d82f-8779-19f7-1498d91f5c8b.jpeg)

Tokenを追加しますと、アクセスできました。
![528923C5-B32E-44B4-A33F-2ED3F4564042_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320164/50934c14-49bc-ebbc-9f9a-95781f65f1da.jpeg)

以上で終わりですが、Django REST frameworkのログイン関連のソースコードの分析も多少いれたいと思います。
興味ある方は読んでください。:relaxed:

# DRFのログインソースコード分析
### 使用する認証classとのリンク
先程書いたコードを元に分析して行きます、
[CBV](https://docs.djangoproject.com/en/3.0/topics/class-based-views/)(Class-based views)使用する場合、`dispatch`は実行されます。
それを入り口として、ソースコード見ていきます。
見方としては、先程書いたログイン用のclassに`self.dispatch()`を追加します。

```python:api/view.py
...
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`関数の中身をみていきます。

```python:rest_framework/views.py
    def dispatch(self, request, *args, **kwargs):
        ...
        # この関数の中身を見てきます
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
```
中身の定義は`rest_framework/views.py`の381行目にあります。

```python:rest_framework/views.py
    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`から取ってきてます。

```python:rest_framework/views.py
    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行目では、以下の定義があります。

```python:rest_framework/views.py
class APIView(View):
   ...
   authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
```
よって、認証class何を使用するのか、直接定義出来る箇所は二つありまして、
1. `APIView`継承した`CBV`classの内部

```python:api/views.py
...
class Login(APIView):
    # ここ
    authentication_classes = [NormalAuthentication, ]

    def post(self, request, *args, **kwargs):
        return Response({"token": request.user})
...
```
2.`settings.py`の`REST_FRAMEWORK`用設定の内部

```python
REST_FRAMEWORK = {
  'DEFAULT_AUTHENTICATION_CLASSES': ['api.utils.auth.NormalAuthentication']
}
```
CBVと認証用classの関連性を把握したら、次は認証用classの機能を見ていきます。
また`dispatch `から辿っていきます、`rest_framework/views.py`の493行の`self.initial(request, *args, **kwargs)`

```python:rest_framework/views.py
    def dispatch(self, request, *args, **kwargs):
        ...

        try:
            self.initial(request, *args, **kwargs)
        ...
```
それの定義を追っていきます。`rest_framework/views.py`の395行目。
`perform_authentication`があります、さらに辿っていきます。

```python:rest_framework.py/views.py
...
    def initial(self, request, *args, **kwargs):
        ...
        self.perform_authentication(request)
        ...
```

その先に`request.user`があります。

```python:rest_framework/views.py
...
    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行にあります。

```python:rest_framework/request.py
...
    @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`になります。

```python:rest_framework/request.py
    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を見てみます。

```python:api/utils/auth.py
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を返します。
ログイン関連ソースの解析は以上となります。

最後まで付き合って頂きありがとうございます。:raised_hand_tone1: