DjangoのMiddlewareについて

Djangoで「APIリクエストの認証処理を自前で作成したいなぁ」と思った時に、Middlewareについて調べたので、その備忘録として記事に残します。


## そもそもMiddlewareって何よ
Djangoの文脈で [Middleware](https://docs.djangoproject.com/ja/2.0/topics/http/middleware/) とは、リクエスト/レスポンス処理にhookを加えるための仕組みです。簡単に言うと、

- リクエストが送られた時に行う処理
- レスポンスを返す前に行う処理

などを定義することができるものです。

（今回、私は特定のURLにアクセスがされたら特別に認証処理を行い、認証が通らなければ200以外のステータスでレスポンスをする、という処理をこのMiddlewareを用いて行いました。）


## Middlewareクラス
### Middlewareクラスの基本形
自前のMiddlewareクラスを作成します。まず、以下のようにベースとなるclass定義を行います。


```python
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ポイントがあります。

```python
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`

```python
...

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関数を用意・呼び出される形でサーバを起動・リクエストを投げてみます。

```python
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
```

```python
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つあります。

1. process_view
2. process_exception
3. process_template_response

これらはMiddlewareクラスのインスタンスメソッドとして定義することで利用することができます。

### process_view
`process_view()` はview関数を呼び出す直前にhookされるメソッドで、(2)のhookポイントと違い、

- 実行されるview関数
- そのview関数への引数（例えばurlsで定義したパラメータ）

を知っているのが特徴です。

`process_view()` は返り値として

- `None`
- `HttpResponse`

のどちらかを返す必要があります。 `None` を返せばそのままview関数への処理が進みますが、 `HttpResponse` を返すと処理はここで止まり、サーバからのレスポンスとしてその `HttpResponse` で定義された内容が返ります。

```python
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で定義されたレスポンスが返ります。

```python
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機構を使うことで、処理をうまく共通化できるようになりそうですね。

## 参考
[ミドルウェア (Middleware) | Django documentation | Django](https://docs.djangoproject.com/ja/2.0/topics/http/middleware/)