Djangoで「APIリクエストの認証処理を自前で作成したいなぁ」と思った時に、Middlewareについて調べたので、その備忘録として記事に残します。
そもそもMiddlewareって何よ
Djangoの文脈で Middleware とは、リクエスト/レスポンス処理にhookを加えるための仕組みです。簡単に言うと、
- リクエストが送られた時に行う処理
- レスポンスを返す前に行う処理
などを定義することができるものです。
(今回、私は特定のURLにアクセスがされたら特別に認証処理を行い、認証が通らなければ200以外のステータスでレスポンスをする、という処理をこのMiddlewareを用いて行いました。)
Middlewareクラス
Middlewareクラスの基本形
自前のMiddlewareクラスを作成します。まず、以下のようにベースとなるclass定義を行います。
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
これがMiddlewareクラスの基本形で、ここから必要に応じて処理を加えていきます。
※実際クラスベースではなく関数ベースのMiddlewareも作成することはできますが、クラスベースを知ってさえすれば困ることはないと思ったため、今回はクラスベースのみを紹介することにしました。
Middlewareクラスに自前の処理を加える
Middlewareクラスには原則として3箇所のhookポイントがあります。
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# (1)
def __call__(self, request):
# (2)
response = self.get_response(request)
# (3)
return response
上記コード内に埋め込んだ(1)〜(3)がhookポイントで、ここに任意の処理を加えていくことになります。
(1): __init__
Middlewareの __init__()
はサーバ起動時に1度だけ呼び出されます。そのため、初期化などの初回のみ実行すべき処理を記述することになると思います。
(2): __call__
get_response実行前
Middlewareの __call__()
はリクエスト毎に呼び出されるメソッドです。そのため、リクエスト毎に実行すべき処理は __call__()
に記述することになります。
self.get_response()
は、request
オブジェクトに対してview関数を適用させてresponse
オブジェクトを得る、ということを行っています。
それゆえ(2)の self.get_response()
実行前に処理を記述することで、view関数適用前に実行する共通処理を定義することができます。
(3): __call__
get_response実行後
(2)と違い、(3)はview関数の処理が終わった後にレスポンスをサーバから返す前に実行する共通処理ということになります。
Middlewareを利用する設定を追加する
上記のMiddlewareクラスを用意しただけではDjangoサーバはそのMiddlewareクラスを利用しません。settings.pyで設定することで用意したMiddlewareクラスを有効にすることができます。
{your project}/settings.py
...
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'{your app}.middleware.SampleMiddleware' # <= 追加
]
...
上の例では {your app}/middleware.py
というファイルに SampleMiddleware
というクラスを用意した場合です。ここの記載は用意した環境によって記載する内容が変わってきますので、環境に合わせて記述するようにしてください。
また、Middlewareはリクエストが送られたらこのsettings.pyに設定されているものを上から順番に(2)に書かれた処理が実行されます。一方で(3)に書かれた処理はこの逆の順番で実行されます。なので場合によってはこの順番を意識して設定する必要があります。
実際にリクエストを送ってみる
以上を踏まえて、以下のようなview関数を用意・呼び出される形でサーバを起動・リクエストを投げてみます。
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
print('(1) init')
def __call__(self, request):
print('(2): before get_response')
response = self.get_response(request)
print('(3): after get_response')
return response
from django.http import HttpResponse
def sample_view(request):
print('in view')
return HttpResponse()
実際にサーバ起動、その後リクエストを2度行った時のログが以下です。(見やすいように改行を入れています)
$ python sampleproj/manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
February 12, 2018 - 10:36:32
Django version 2.0.2, using settings 'sampleproj.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
(1): init
(2): before get_response
in view!
(3): after get_response
[12/Feb/2018 10:39:25] "GET /sample HTTP/1.1" 200 17
(2): before get_response
in view!
(3): after get_response
[12/Feb/2018 10:39:29] "GET /sample HTTP/1.1" 200 17
(1)は一度だけ、(2)・(3)はリクエストの度に呼ばれていることがわかりますね。
特殊メソッド
Middlewareには今まで述べてきた(1)~(3)の他にhookポイントが3つあります。
- process_view
- process_exception
- process_template_response
これらはMiddlewareクラスのインスタンスメソッドとして定義することで利用することができます。
process_view
process_view()
はview関数を呼び出す直前にhookされるメソッドで、(2)のhookポイントと違い、
- 実行されるview関数
- そのview関数への引数(例えばurlsで定義したパラメータ)
を知っているのが特徴です。
process_view()
は返り値として
None
HttpResponse
のどちらかを返す必要があります。 None
を返せばそのままview関数への処理が進みますが、 HttpResponse
を返すと処理はここで止まり、サーバからのレスポンスとしてその HttpResponse
で定義された内容が返ります。
from django.http import HttpResponse
class HttpResponseBadRequest(HttpResponse):
status_code = 400
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
if view_func.__name__.startswith('api'):
return HttpResponseBadRequest()
else:
return None
この例では、view関数の関数名が api
から始まる関数だった場合は、HTTPステータスコードが400のレスポンスを返し、それ以外はそのまま通常の処理を進めるという処理になります。(例ですので、実用性とか全く考えていないのであしからず。)
process_exception
process_exception()
はview関数内で例外(Exception)がraiseされた時にhookされるメソッドです。
None
HttpResponse
を返すことができます。None
の場合はそのまま処理が進み、HttpResponse
を返した場合はそのHttpResponseで定義されたレスポンスが返ります。
from django.http import HttpResponse
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
return response
def process_exception(self, request, exception):
same_logger(exception)
return HttpResponse('error raised')
この例では、なんらかのロガーでexceptionの内容を記録し、error raised
という内容のレスポンスを200番で返しています。
process_template_response
process_template_response()
は、view関数の処理の結果、 TemplateResponse
を返す場合にのみhookされるメソッドです。例えば django.shortcut.render
を使って return render(...)
などと返しているview関数が該当します。
process_template_response()
の特徴は
- 返される
TemplateResponse
オブジェクト
を知っているのが特徴です。
これらの特殊メソッドのhook機構を使うことで、処理をうまく共通化できるようになりそうですね。