はじめに
WebアプリをSPAで実装したときにサーバーとのデータのやり取りはWebAPIになり、JSONでのデータの受け渡しが主だと思います。
Djangoでサーバーを構築するとWebAPIもとても簡単に実装することができます。
Webアプリにログイン機能などをもたせたい場合、その管理方法に思慮することになりますが、DjangoのRestFrameworkを使用すれば、認証などはデフォルトで色々と用意されています。
今回実現したいのは以下のような機能です。
- メールアドレスとパスワードによるログイン
- ユーザ情報はDjangoのUserモデルを使用する
- 無操作によるタイムアウトをサーバー側で管理する
- 同一ユーザーの重複ログインは不可とする(後勝ち)
Webアプリではよくあるような仕様ですが、これをDjangoのWebAPIではどのように実装すればよいのかを考えました。
同一ユーザーの重複ログインは先勝ちが理想ですが、経験上それは難しいので後勝ちとします。
TokenAuthencationの拡張でも実装は可能だと思いますが、勉強も兼ねてAuthenticationを独自に実装することにしました。
Python、Djangoともに経験が1ヶ月程度ですので、問題などはご指摘いただけると非常に助かります。
検証環境
OS:Windows7 Professional
Pythonがすでにインストールされていて、パスが登録されていること。
Macの場合はコマンドなどを少々読み替えてください。ソースコードは同じで問題ありません。
$ python --version
Python 3.6.6
仮想環境管理にvevnを使用していますが、必須ではありません。使用しない場合はvenvの記載は無視して問題ありません。
Djangoプロジェクトの作成と設定
プロジェクトの作成
venvでプロジェクト管理したいので、プロジェクトフォルダを作成して、仮想環境を作成。
プロジェクトフォルダ作成
$ mkdir example_auth
プロジェクトフォルダに移動
$ cd example_auth
仮想環境作成(venvを使用する場合)
$ python -m venv venv
仮想環境を有効化(venvを使用する場合)
$ venv\Scripts\activate
仮想環境ができたところで、djangoとrest-frameworkをインストールします。
djangoとrest-frameworkをインストール
$ pip install django djangorestframework
(省略)
インストールされたパッケージを確認
$ pip freeze
Django==2.1.1
djangorestframework==3.8.2
pytz==2018.5
djangoのプロジェクトを作成する。
$ django-admin.py startproject example_auth .
最後にピリオドを入力することで、カレントフォルダにプロジェクトを作成します。
プロジェクトを作成すると、以下のようなフォルダ構成になります。
example_auth
├ manage.py
├ example_auth
│ ├ __init__.py
│ ├ settings.py
│ ├ urls.py
│ └ wsgi.py
└ venv
└ (割愛)
プロジェクトの作成が完了しました。
データベース作成
本記事ではデフォルトのSQLiteを使用します。
$ manage.py migrate
(省略)
カレントフォルダにdb.sqlite3というファイルが作成されます。
スーパーユーザーの作成
以下のコマンドでスーパーユーザーを作成します。
$ manage.py createsuperuser
Username (leave blank to use 'admin'): admin
Email address: admin@example.com
Password: admin
Password (again): admin
(省略)
Superuser created successfully.
2回目のパスワードを入力したあとに「パスワードが簡単すぎる」と言われますが、無視して「y」と入力してください。
スーパーユーザーの設定値は任意ですが、上記の設定が行われた前提で、話を進めます。
言語とタイムゾーンの設定
example_authフォルダの中にあるsettings.pyの言語とタイムゾーンの設定を編集します。
LANGUAGE_CODE = 'ja' # en-us → ja
TIME_ZONE = 'Asia/Tokyo' # UTC → Asia/Tokyo
Webサーバーの起動
以下のコマンドでWebサーバーを起動します。
$ manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
September 05, 2018 - 12:59:10
Django version 2.1.1, using settings 'example_auth.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
ブラウザから「http://127.0.0.1:8000/」にアクセスしてWebサーバーが起動していることを確認します。
また「http://127.0.0.1/admin/」にアクセスして、管理画面に先ほど作成したスーパーユーザーでログインできることを確認します。
WebAPI作成の準備
アプリケーションの作成
今回は通常のWebサーバーが扱うHtmlではなく、WebAPIでJSONの送受信を行うアプリケーションを生成します。アプリケーション名は「api」とします。
以下のコマンドでアプリケーションを作成します。
$ manage.py startapp api
以下のフォルダとファイルが作成されます。
api
├ __init__.py
├ admin.py
├ apps.py
├ admin.py
├ models.py
├ tests.py
├ views.py
└ migrations
└ __init__.py
作成したアプリケーションをsettings.pyに登録します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'api', # 追加
]
モデルの作成
アプリケーションを作成する準備が整ったので、まずトークンを管理するクラスを作成します。トークンはユーザーごとに管理するため、このクラスはユーザーと紐付かせます。
from django.db import models
from django.contrib.auth.models import User
class ExampleToken(models.Model):
# 紐付くユーザー
user = models.ForeignKey(User, on_delete=models.CASCADE)
# アクセストークン
token = models.CharField(max_length=40)
# アクセス日時
access_datetime = models.DateTimeField()
def __str__(self):
# メールアドレスとアクセス日時、トークンが見えるようにする
dt = timezone.localtime(self.access_datetime).strftime("%Y/%m/%d %H:%M:%S")
return self.user.email + '(' + dt + ') - ' + self.token
クラスのメンバ変数は以下の用途で使用します。
user : トークンに紐づくユーザー。ユーザーとは1:1の関係とします。
token : アクセストークン。ログイン後はこのトークンを使用してユーザーの認証を行います。max_lengthが40に設定されている理由は、トークンはsha1でハッシュ化した文字列を設定するためです。
access_datetime : 最後にアクセスした日時を格納します。サーバーにアクセスするたびに更新します。
次に作成したモデルをデータベースに反映します。
$ manage.py makemigrations
(省略)
$ manage.py migrate
(省略)
モデルを管理画面に追加
作成したモデルを管理画面で参照できるようにadmin.pyも編集します。
from django.contrib import admin
from .models import ExampleToken
class ExampleTokenAdmin(admin.ModelAdmin):
fields = ['user', 'token', 'access_datetime',]
admin.site.register(ExampleToken, ExampleTokenAdmin)
設定が出来たらブラウザから設定画面にアクセスして、ExampleTokenモデルが追加されていることを確認します。データベースにはまだレコードが出来ていないので、中身は空っぽです。
ログイン機能の作成
データベースが完成したので、次にログイン機能を作成します。
ログイン機能の仕様としては、メールアドレスとパスワードがJSON形式でポストされ、一致するユーザーがいた場合はExampleTokenにレコードを追加してトークンを返却します。一致するユーザーがいない場合はエラーを返却します。
WebAPIでの実装となるため、rest-frameworkをsettings.pyに追加します。
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # 追加
'api',
]
ログイン処理の作成
ログインしたらExampleTokenにレコードを追加しますので、まずその処理を作成します。
実装方法は色々あると思いますが、個人的な趣味でExampleTokenクラスの静的メソッドとして作成します。models.pyを以下のように編集してください。
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone # 追加
import hashlib # 追加
class ExampleToken(models.Model):
# このメソッドを追加
@staticmethod
def create(user: User):
# ユーザの既存のトークンを取得
if ExampleToken.objects.filter(user=user).exists():
# トークンが既に存在している場合は削除する
ExampleToken.objects.get(user=user).delete()
# トークン生成(メールアドレス + パスワード + システム日付のハッシュ値とする)
dt = timezone.now()
str = user.email + user.password + dt.strftime('%Y%m%d%H%M%S%f')
hash = hashlib.sha1(str.encode('utf-8')).hexdigest() # utf-8でエンコードしないとエラーになる
# トークンをデータベースに追加
token = ExampleToken.objects.create(
user = user,
token = hash,
access_datetime = dt)
return token
importが追加になっているのと、createメソッドを新たに作成しました。
ユーザーを引数として呼び出し元から渡してもらい、そのユーザーに対するトークンを作成します。
トークンは使い捨てとして、ログイン時に新しく作成します。古いトークンがあった場合は削除します。
トークンは同一のものが存在すると困るので、一致しなそうな文字列をsha1でハッシュ化した文字列を使用しています。
作成したcreateメソッドを使用して、ログイン処理のWebAPIを作成します。views.pyを以下のように編集してください。
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
from .models import ExampleToken
import json
class Login(APIView):
def post(self, request, format=None):
# リクエストボディのJSONを読み込み、メールアドレス、パスワードを取得
try:
data = json.loads(request.body)
email = data['email']
password = data['password']
except:
# JSONの読み込みに失敗
return JsonResponse({'message': 'Post data injustice'}, status=400)
# メールアドレスからユーザを取得
if not User.objects.filter(email=email).exists():
# 存在しない場合は403を返却
return JsonResponse({'message': 'Login failure.'}, status=403)
user = User.objects.get(email=email)
# パスワードチェック
if not user.check_password(password):
# チェックエラー
return JsonResponse({'message': 'Login failure.'}, status=403)
# ログインOKの場合は、トークンを生成
token = ExampleToken.create(user)
# トークンを返却
return JsonResponse({'token': token.token})
作成したWebAPIをアクセス可能とするため、urls.pyを編集します。
from django.contrib import admin
from django.urls import path, include # includeを追加
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')), # 追加
]
apiフォルダにurls.pyを新たに作成し、以下の内容に編集してください。
from django.urls import path
from .views import Login
urlpatterns = [
path('login', Login.as_view(), name='login'),
]
これで「http://127.0.0.1:8000/api/login」にアクセスが可能になりました。
許容するメソッドはPOSTのみですので、普通にブラウザでアクセスしてもエラーとなります。
本記事でのWebAPIへのアクセスはcurlを使用します。(Windowsには標準で入っていないので、別途インストールが必要です。)
ログインAPIの実行
以下のコマンドで作成したログインAPIを実行します。
リクエストメソッドはPOSTで、POSTデータとしてメールアドレスとパスワードを送信します。
$ curl -i -X POST -d "{\"email\":\"admin@example.com\",\"password\":\"admin\"}" http://127.0.0.1:8000/api/login
HTTP/1.1 200 OK
Date: Wed, 05 Sep 2018 05:39:12 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept, Cookie
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 53
{"token": "356dc257775c0fbf50bc0855c113de0065005f72"}
ステータスコードが200が返却され、トークンが取得できました。
この状態で管理画面でExampleTokenにレコードが追加されたことが確認できます。
もちろん、存在しないメールアドレスや、パスワードが間違っている場合は403エラーとなります。
今後はこのAPIで取得したトークンをサーバーに送信して認証を行います。
認証処理の作成
認証処理用のAPIを作成
認証処理を作成するために、まず簡単なAPIを作成します。
アクセスされたら「Yes」というメッセージを返すだけのイエスマンAPIです。以下のクラスをviews.pyに追加してください。
class YesMan(APIView): # YesManクラスを追加
def post(self, request, format=None):
return JsonResponse({'message': 'Yes'})
このAPIにアクセスするためにurls.pyを編集します。
from django.urls import path
from .views import Login, YesMan # YesManを追加
urlpatterns = [
path('login', Login.as_view()),
path('yesman', YesMan.as_view()), # 追加
]
curlで追加したAPIにアクセスします。
$ curl -X POST http://127.0.0.1:8000/api/yesman
{"message": "Yes"}
当然ですが、このままでは認証はおろか、リクエストにトークンを含めなくても「Yes」と言われます。
では、認証されていないユーザには「Yes」と言われないようにします。
認証される条件は以下とします。
- リクエストに正しいトークンが含まれること
- トークンの最終アクセス日時から30分以内であること
上記のチェック処理をExampleTokenクラスに追加します。
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from datetime import timedelta
import hashlib
class ExampleToken(models.Model):
# 既存のコードは変更なし。以下のコードを追加
@staticmethod
def get(token_str: str):
# 引数のトークン文字列が存在するかチェック
if ExampleToken.objects.filter(token=token_str).exists():
# 存在した場合はトークンを返却
return ExampleToken.objects.get(token=token_str)
else:
# 存在しない場合はNoneを返却
return None
def check_valid_token(self):
# このトークンが有効かどうかをチェック
delta = timedelta(minutes=30) # 有効時間は30分
if(delta < timezone.now() - self.access_datetime):
# 最終アクセス時間から30分以上経過している場合はFalseを返却
return False
return True
def update_access_datetime(self):
# 最終アクセス日時を現在日時で更新
self.access_datetime = timezone.now()
self.save()
追加したメソッドの使い方は以下のようなイメージです。
- クライアントからトークン文字列が送られてくるので、getメソッドを使用してトークン文字列からトークンクラスのインスタンスを取得。
- トークンの有効期限は最終アクセス時間から30分なので、check_valid_tokenメソッドで有効かどうかをチェック。
- トークンが有効だった場合は、update_access_datetimeメソッドを使用して、最終アクセス時間を現在日時で更新。
これらのメソッドを使用して、認証機能を作成します。
認証機能の作成
apiフォルダ内にauthentication.pyを新たに作成して、内容を以下のように編集します。
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
from rest_framework import status
from .models import ExampleToken
class ExampleAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
# リクエストヘッダからトークン文字列を取得
token_str = request.META.get('HTTP_X_AUTH_TOKEN')
if not token_str:
# リクエストヘッダにトークンが含まれない場合はエラー
raise exceptions.AuthenticationFailed({'message': 'token injustice.'})
# トークン文字列からトークンを取得する
token = ExampleToken.get(token_str)
if token == None:
# トークンが取得できない場合はエラー
raise exceptions.AuthenticationFailed({'message': 'Token not found.'})
# トークンが取得できた場合は、有効期間をチェック
if not token.check_valid_token():
# 有効期限切れの場合はエラー
raise exceptions.AuthenticationFailed({'message': 'Token expired.'})
# トークンが有効な場合は、アクセス日時を更新
token.update_access_datetime()
return (token.user, None)
リクエストヘッダからトークン文字列を取得し、そのトークン文字列の整合性をチェックします。チェックNGの場合は、例外をスローして認証失敗としています。
認証機能を作成しただけではAPIには適応されないため、先ほど作成したイエスマンに認証機能を付与します。
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated # 追加
from .models import ExampleToken
from .authentication import ExampleAuthentication # 追加
import json
# Loginクラスは修正なし
class YesMan(APIView):
authentication_classes = (ExampleAuthentication,) # 追加
permission_classes = (IsAuthenticated,) # 追加
def post(self, request, format=None):
return JsonResponse({'message': 'Yes'})
authentication_classesで使用する認証クラスを指定します。
permission_classesにはこのAPIを使用するための権限を設定します。IsAuthenticatedはauthentication_classesで設定した認証が行えた場合にこのAPIにアクセス可能となります。
この状態で、先ほどのcurlで送信したリクエストを、もう一度送信します。
$ curl -i -X POST http://127.0.0.1:8000/api/yesman
HTTP/1.1 403 Forbidden
Date: Wed, 05 Sep 2018 08:17:42 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 30
{"message":"token injustice."}
今度は認証機能が働いているため、トークン不正でステータスコード403が返却されました。
リクエストヘッダにトークンを含めて送信してみます。ここで送信するトークンは、ログインAPIで返却されたトークンです。
$ curl -i -X POST -H "X-AUTH-TOKEN:356dc257775c0fbf50bc0855c113de0065005f72" http://127.0.0.1:8000/api/yesman
HTTP/1.1 403 Forbidden
Date: Wed, 05 Sep 2018 08:19:52 GMT
Server: WSGIServer/0.2 CPython/3.6.6
Content-Type: application/json
Vary: Accept
Allow: POST, OPTIONS
X-Frame-Options: SAMEORIGIN
Content-Length: 28
{"message":"Token expired."}
先ほどログインした時間から30分以上経過している場合は、トークンの有効期限切れでエラーとなります。
こうなった場合は、再度ログインして新しいトークンを取得してからイエスマンを呼ぶと、正常に「Yes」と返事をしてくれます。
$ curl -X POST -d "{\"email\":\"admin@example.com\",\"password\":\"admin\"}" http://127.0.0.1:8000/api/login
{"token": "7348aa895d486360369652c1121538b900a819e3"}
C:\curl>curl -X POST -H "X-AUTH-TOKEN:7348aa895d486360369652c1121538b900a819e3" http://127.0.0.1:8000/api/yesman
{"message": "Yes"}
認証に成功したユーザー情報は、request.userで使用することが出来ます。
イエスマンAPIに認証成功したユーザーのメールアドレスもレスポンスに含めるには以下のようにします。
class YesMan(APIView):
authentication_classes = (ExampleAuthentication,)
permission_classes = (IsAuthenticated,)
def post(self, request, format=None):
return JsonResponse({'message': 'Yes', 'email': request.user.email}) # 修正
request.userにはExampleAuthenticationクラスのauthenticationメソッドが返却したユーザー情報が格納されます。
$ curl -X POST -H "X-AUTH-TOKEN:7348aa895d486360369652c1121538b900a819e3" http://127.0.0.1:8000/api/yesman
{"message": "Yes", "email": "admin@example.com"}
認証処理をデフォルト設定にする
現在の状態は、認証を行いたいAPIに対して、authentication_classesとpermission_classesを登録していますが、大規模なアプリケーションになると、すべてのAPIに対して登録するのは冗長なので、デフォルトで認証が行われるようにすることも出来ます。
setting.pyに以下のコードを追加します。
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'api.authentication.ExampleAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
これでデフォルトで認証が行われるようになったので、API単位での認証設定を変更します。
from django.contrib.auth.models import User
from django.http.response import JsonResponse
from rest_framework.views import APIView
# from rest_framework.permissions import IsAuthenticated # 削除
from .models import ExampleToken
# from .authentication import ExampleAuthentication # 削除
import json
class Login(APIView):
authentication_classes = () # 追加
permission_classes = () # 追加
# 省略
class YesMan(APIView):
# authentication_classes = (ExampleAuthentication,) # 削除
# permission_classes = (IsAuthenticated,) # 削除
def post(self, request, format=None):
return JsonResponse({'message': 'Yes', 'email': request.user.email})
YesManクラスからは認証設定を削除しています。
Loginクラスには空の認証情報を追加していますが、ログイン時には認証は不要なので、デフォルトの認証を打ち消しています。
以上でWebAPIを使用した独自のトークン認証の実装は終了です。
BaseAuthenticationクラスを継承したクラスの作り次第で、どんな認証も可能だと思います。
WebAPIのテスト
以下のように、APITestCaseクラスを使用すると簡単にテストコードが作成できます。
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APITestCase
from .models import ExampleToken
from datetime import timedelta
from time import sleep
import json
def create_user():
admin = User.objects.create_superuser('admin', 'admin@example.com', 'admin')
user = User.objects.create_user('test', 'test@example.com', 'test')
return (admin, user)
def create_token():
admin = User.objects.get(username='admin')
ExampleToken.objects.create(
user = admin,
token = '1111111111222222222233333333334444444444',
access_datetime = timezone.now()
)
test = User.objects.get(username='test')
ExampleToken.objects.create(
user = test,
token = 'aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd',
access_datetime = timezone.now() - timedelta(minutes=29.9)
)
def create_token_expired():
admin = User.objects.get(username='admin')
ExampleToken.objects.create(
user = admin,
token = '1111111111222222222233333333334444444444',
access_datetime = timezone.now() - timedelta(minutes=30)
)
class ExampleTokenTests(TestCase):
def test_create_token(self):
(admin, user) = create_user()
# adminのトークンを作成
ExampleToken.create(admin)
# トークンが作成されたかを確認
self.assertTrue(ExampleToken.objects.filter(user=admin).exists(), 'Token not created.')
# トークンの長さが40文字であるかを確認
token = ExampleToken.objects.get(user=admin)
self.assertEqual(len(token.token), 40, 'Token length not 40.')
# トークン再作成でトークンが変更するかを確認
token_old = token.token
sleep(0.1) # 同一時刻だとトークンも同じなので、一瞬待機
token = ExampleToken.create(admin)
self.assertNotEqual(token_old, token.token)
class LoginTests(APITestCase):
def test_login_success(self):
create_user()
# スーパーユーザーのログイン確認
response = self.client.post(reverse('login'), {'email':'admin@example.com','password':'admin'}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))
# 一般ユーザーのログイン確認
response = self.client.post(reverse('login'), {'email':'test@example.com','password':'test'}, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))
def test_login_failure(self):
create_user()
# POSTデータなし(400エラー)
response = self.client.post(reverse('login'))
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))
# POSTデータにパスワードなし(400エラー)
response = self.client.post(reverse('login'), {'email':'admin@example.com'}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))
# POSTデータにメールアドレス無し(400エラー)
response = self.client.post(reverse('login'), {'password':'admin'}, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST, json.loads(response.content))
# メールアドレス不正(403エラー)
response = self.client.post(reverse('login'), {'email':'dammy@example.com','password':'admin'}, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
# パスワード不正(403エラー)
response = self.client.post(reverse('login'), {'email':'admin@example.com','password':'dummy'}, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
class YesManTests(APITestCase):
def test_yesman_success(self):
create_user()
create_token()
# トークン作成直後
response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='1111111111222222222233333333334444444444')
self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))
# トークンの有効期限すれすれ
response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd')
self.assertEqual(response.status_code, status.HTTP_200_OK, json.loads(response.content))
def test_yesman_failure(self):
create_user()
create_token_expired()
# トークンが含まれない(403エラー)
response = self.client.post(reverse('yesman'))
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
# トークンが間違っている(403エラー)
response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
# トークンの有効期限が切れている(403エラー)
response = self.client.post(reverse('yesman'), HTTP_X_AUTH_TOKEN='1111111111222222222233333333334444444444')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN, json.loads(response.content))
ExampleTokenのテストは途中で面倒になって手抜きです。