Django Rest Framework で JWTによるAPIの認証機能を実装

# 概要

　この記事は初心者の自分がRESTful なAPIとswiftでiPhone向けのクーポン配信サービスを開発した手順を順番に記事にしています。技術要素を1つずつ調べながら実装したため、とても遠回りな実装となっています。

前回の [【Django Rest Framework】Django-Filterを使ってフィルタ機能をカスタマイズする](https://qiita.com/Ajyarimochi/items/a88331825667ce27cf48) でリクエストするデータをDjango-Filtersを使って細かくフィルタリングする方法がわかりました。次はJson Web Token（JWT）を使ったAPIの認証機能を追加します。認証機能が無いと誰でもクーポンデータを追加、変更、削除できてしまいます。


### 参考

* [Django REST framework で django-filter を使う](https://qiita.com/okoppe8/items/77f7f91f6878e3f324cc)
* [Django-Filter リファレンス](https://django-filter.readthedocs.io/en/latest/ref/filters.html#core-arguments)
* [Django REST framework リファレンス](https://www.django-rest-framework.org)
* [Django REST Frameworkでユーザ認証周りのAPIを作る](https://qiita.com/xKxAxKx/items/60e8fb93d6bbeebcf065)
* [ジャンゴ(django)でJWTを使う方法](https://dev-yakuza.github.io/django/jwt/)


### 環境

Mac OS 10.15
VSCode 1.39.2
pipenv 2018.11.26
Python 3.7.4
Django 2.2.6


# 手順

* パッケージのインストール
* settings.pyにJWT認証の設定を追加
* urls.py にJWT認証の設定を追加
* JWTのアクセストークンを取得
* 動作確認
* APIを利用するクーポンアプリ側の修正（必要な方のみ）
 * リクエストURLを変更
 * 表示するデータを取り出す際のjsonのキーの変更
 * 動作確認

### パッケージのインストール

　Django Rest Frameworkの環境でJWT認証を構築するにはdjango-rest-framework-jwtというパッケージが必要なのでインストールします。

　自分は pipenv で Djangoの仮想環境を構築しており、その環境下では下記のコマンドでインストールします。インストールは成功しましたが、少し時間がかかりました。

```
$ pipenv install djangorestframework-jwt
```


### settings.pyにJWT認証の設定を追加

 `REST_FRAMEWORK = { ～`の所にJWT認証の設定を2つ追加します。

1つは `DEFAULT_PERMISSION_CLASSES` で、アクセス許可を判断するクラスを指定します。views.py の処理を実行する際に判断します。指定したクラス全てで許可される必要があります。

指定できるクラスは以下の2つあります。

* `IsAuthenticated`：全てのリクエストに対して認証が必要なクラスです。
* `IsAuthenticatedOrReadOnly` : 認証なしでも読み取り専用のリクエスト（つまりGET）に限って許可するクラスです。

今回は `'rest_framework.permissions.IsAuthenticatedOrReadOnly'`を指定します。



2つ目は`DEFAULT_AUTHENTICATION_CALSSES` で、認証に使うクラスを指定します。ここは`rest_framework_jwt.authentication.JSONWebTokenAuthentication`を指定します。`JSONWebTokenAuthentication` はJWT認証のクラスです。

`REST_FRAMEWORK = { ～`の所は下記のようになりました。

```python

REST_FRAMEWORK = {
    # フィルタの追加
    'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),

    # JWT認証の追加
    'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAuthenticated',),
    'DEFAULT_AUTHENTICATION_CLASSES': ('rest_framework_jwt.authentication.JSONWebTokenAuthentication',),
}
```

次に`JWT_AUTH = {   }` を追加し、トークンの期限に関する設定をします。今回は無期限になるように設定しました。

```python

JWT_AUTH = {
    # トークンの期限を無効に設定
    'JWT_VERIFY_EXPIRATION': False,
}
```


### urls.py にJWT認証の設定を追加

　プロジェクト名のディレクトリ配下の`ulrs.py`を編集します。

まず`rest_framework_jwt.views`の`obtain_jwt_token`をインポートします。

次に、トークン発行用のURLの設定を追加します。リクエストURLのエンドポイントに`api-auth`が設定された際に`obtain_jwt_token`を参照するようにします。

```python:ami_coupon_api/urls.py

from django.contrib import admin
from django.urls import path,include
from django.conf.urls import url, include
from coupon.urls import router as coupon_router
from rest_framework_jwt.views import obtain_jwt_token # JWT認証のために追加

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^api/', include(coupon_router.urls)),
    url(r'^api-auth/', obtain_jwt_token), # 認証のためのURL
]
```

### JWTのアクセストークンを取得

　以上でJWTのアクセストークンを発行出来るようになりました。下記のリクエストでトークンを発行します。URLのエンドポイントには `urls.py` で追加したパスを設定します。usernameとpasswordはDjangoサーバーにログインする際に使うスーパーユーザーのものです。

```
curl http://127.0.0.1:8000/api-auth/ -d "username=XXXXXX&password=YYYYYY”
```

成功すると`{“token” : “[トークンの中身]”}`の形式で、長いトークンが返ってきます。認証に必要なのでセキュリティが確保された場所に保管しておきます。


### 動作確認

　GET、POST、PUT、DELETEのリクエストをそれぞれ投げてみます。

**GET**

GETリクエストは認証無しで可能なはずなのでトークンは設定しません。

```
$ curl -X GET http://127.0.0.1:8000/api/coupons/
```

Jsonが返ってきました。

[{"id":1,"code":"0001","benefit":"お会計から1,000円割引","explanation":"5,000円以上ご利用のお客様限定。他クーポンとの併用不可。","store":"全店","start":"2019-10-01","deadline":"2019-12-31","status":true},{"id":2,"code":"0002","benefit":"お会計を10%オフ！","explanation":"他クーポンとの併用不可","store":"有楽町店","start":"2019-10-01","deadline":"2019-12-31","status":true},
--以下略--

**POST**

POSTリクエストは認証が必要なので、まずはトークンを設定せずにリクエストしてみます。

```
$ curl -X POST http://127.0.0.1:8000/api/coupons/ -d "code=0007" -d "benefit=お会計から19%引き" -d "explanation=１２月２９日～１２月３１日限定。 " -d "store=神田店" -d "start=2019-12-29" -d "deadline=2019-12-31" -d "status=true"
```

トークンを設定していないのでリクエストが通りませんでした。以下のようなエラーが返ってきます。

{"detail":"Authentication credentials were not provided."}



次にトークンを設定してリクエストしてみます。

```
curl -X POST http://127.0.0.1:8000/api/coupons/ -d "code=0007" -d "benefit=お会計から19%引き" -d "explanation=１２月２９日～１２月３１日限定。 " -d "store=神田店" -d "start=2019-12-29" -d "deadline=2019-12-31" -d "status=true" -H "Authorization: JWT [トークン]"
```

リクエストが通りました。

{"id":9,"code":"0007","benefit":"お会計から19%引き","explanation":"１２月２９日～１２月３１日限定。","store":"神田店","start":"2019-12-29","deadline":"2019-12-31","status":true}


**PUT**

PUTリクエストも認証が必要なので、まずはトークンを設定せずにリクエストしてみます。（”code=0007” を ”code=0008” へ変更）

```
curl -X PUT http://127.0.0.1:8000/api/coupons/7/ -d "code=0008" -d "benefit=お会計から19%引き" -d "explanation=１２月２９日～１２月３１日限定。他のクーポンとの併用不可 " -d "store=神田店" -d "start=2019-12-29" -d "deadline=2019-12-31" -d "status=true"
```

トークンを設定していないのでリクエストが通りませんでした。エラーメッセージが返ってきます。


{"detail":"Authentication credentials were not provided."}


次にトークンを設定してリクエストしてみます。

```
curl -X PUT http://127.0.0.1:8000/api/coupons/7/ -d "code=0008" -d "benefit=お会計から19%引き" -d "explanation=１２月２９日～１２月３１日限定。他のクーポンとの併用不可 " -d "store=神田店" -d "start=2019-12-29" -d "deadline=2019-12-31" -d "status=true" -H "Authorization: JWT [トークン]"
```

リクエストが通りました。

{"id":7,"code":"0008","benefit":"お会計から19%引き","explanation":"１２月２９日～１２月３１日限定。他のクーポンとの併用不可","store":"神田店","start":"2019-12-29","deadline":"2019-12-31","status":true}


**DELETE**

DELETEリクエストも認証が必要なので、まずはトークンを設定せずにリクエストしてみます。

```
curl -X DELETE http://127.0.0.1:8000/api/coupons/9/
```

トークンを設定していないのでリクエストが通りませんでした。エラーメッセージが返ってきます。

{"detail":"Authentication credentials were not provided."}


次にトークンを設定してリクエストしてみます。

```
curl -X DELETE http://127.0.0.1:8000/api/coupons/9/ -H "Authorization: JWT [トークン]"
```

リクエストが通りました。（DELETEなので成功したら何も返ってきません）



# APIを利用するクーポンアプリ側を修正　（必要な方のみ）

　ここから先は本記事を参考にAPIとswiftでiPhone向けのクーポン配信サービスを開発されている方のみ関係する内容です。APIの仕様が変わったのでアプリ側を修正する必要が生じました。

### 手順

* リクエストURLを変更
* 表示するデータを取り出す際のjsonのキーの変更
* 動作確認


### リクエストURLを変更

　APIのリクエストURLが変わったので、`override func viewDidLoad()`で設定しているAPIのリクエストURLを変更します。

**変更前**
`http://127.0.0.1:8000/coupon/`

**変更後**
`http://127.0.0.1:8000/api/coupons/`

改修箇所は`viewDidLoad()`で変数`url`にURLを設定している部分。

```ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        let url: URL = URL(string: "http://127.0.0.1:8000/api/coupons/")!
        let task: URLSessionTask = URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in

// 以下略

```


### 表示するデータを取り出す際のjsonのキーの変更

　Django Rest Framework を導入した際にjsonのキーが変わったので、`tableView`でキーを指定してjsonのバリューを取り出している部分を修正します。

　ちなみにキーが変わった理由は、それまで自分の好きなようにキーの名前を指定していたのが、Django Rest Frameworkを導入した事でキーがモデルフィールド名に統一されたためです。プログラムの可読性の観点からするとキーには初めからモデルフィールド名を設定しておくべきだったと思います。

　改修箇所は`tableView`の各ラベルに値を設定している所で、辞書型変数`coupon[“jsonのキー”]`の所です。

　修正後はこのようになります。個人によってモデルフィールド名は違うと思うので、curl で レスポンスのjsonを確認して設定するのが確実です。

```swift

         //各ラベルに値を設定する
        let labelBenefit = cell.viewWithTag(1) as! UILabel
        labelBenefit.text = (coupon["benefit"] as! String)

        let labelExplanation = cell.viewWithTag(2) as! UILabel
        labelExplanation.text = (coupon["explanation"] as! String)

        let labelStore = cell.viewWithTag(3) as! UILabel
        labelStore.text = (coupon["store"] as! String)

        let labelDay = cell.viewWithTag(4) as! UILabel
        labelDay.text = "有効期間： " + (coupon["start"] as! String) + " ～ " + (coupon["deadline"] as! String)
```

### 動作確認

　修正を保存してXcodeでアプリを実行します。するとシミュレータにこのように表示されました。改修は成功です。

<img width="395" alt="check-app-view.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526594/be7a76d1-e4e5-9ce4-9e9f-527fd2aecc80.png">



以上でAPIの認証も実装し、ある程度実用に耐えるサービスになってきました。ただクーポンは文字だけで飾り気が無く訴求力に劣る気がします。

そこで次回は[APIでクーポンの画像を配信できるようにします](https://qiita.com/Ajyarimochi/items/b7278c357b2ffca183b5)