Djangoで構築したWEBサーバに最低限のセキュリティ対策を施す ① ログイン認証

#はじめに
本稿ではDjangoで構築したサーバに以下の最低限のセキュリティ対策を行う手順を解説します。(DjangoでWebアプリケーション構築済である前提とします。)

- ログイン認証の追加
- SSL対応 (記事作成中)

#ログイン用アプリケーションの作成
まずプロジェクト配下でログイン機能用のアプリケーションを作成する。

```
$ python manage.py startapp accounts
```

アプリケーション作成後のプロジェクト構成は以下。

```
sample_project
├── accounts <- ログイン用アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── urls.py
│   └── views.py
├── sample_app <- 固有アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
```

プロジェクト配下のsetting.pyとurls.pyにログイン用アプリケーション用の設定を追記。

```python:setting.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'sample_app.apps.SampleAppConfig', # Custom App
    'accounts.apps.AccountsConfig',    # 追加
]
```

```python:config/urls.py
urlpatterns = [
    path('admin/', admin.site.urls),
    path('sample_app/', include('sample_app.urls')), # Custom App
    path('', include('accounts.urls')),              # 追加 ログイン用
]
```

#ログイン用フォーム、ビュー、テンプレートの作成
以下、ログイン画面の部品をそれぞれ作成する。

- ログイン用フォームの定義：forms.py
- ログイン用ビューの定義：view.py
- ログイン画面用テンプレート：login.html

フォームの定義はDjango標準クラスのAuthenticationFormを用いる。

```python:forms.py
from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
    """ログインフォーム"""
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = 'form-control'
            field.widget.attrs['placeholder'] = field.label   
```

importしているクラスは以下。

|  クラス名  |  機能  |
| ---- | ---- |
|  LoginRequiredMixin  |  ログインしたユーザだけ閲覧できるようにする  |
|  LoginView  |  ログイン機能  |
|  LogoutView  |  ログアウト機能  |
|  LogoutForm  |  上記で定義したログイン用フォーム  |


```python:view.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import(LoginView, LogoutView)
from .forms import LoginForm

class Login(LoginView):
    """ログインページ"""
    form_class = LoginForm
    template_name = 'login.html'


class Logout(LoginRequiredMixin, LogoutView):
    """ログアウトページ"""
    template_name = 'login.html'
```

```html:login.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>LOGIN</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <form action="" method="POST">
        {% csrf_token %}
        {% for field in form %}
            {{ field }}
        {% endfor %}
        {% for error in form.non_field_errors %}
            {{ error }}
        {% endfor %}
        <button type="submit">ログイン</button>
    </form>
</body>
</html>
```

フォーム、ビュー、テンプレート追加後のプロジェクト構成は以下。

```
sample_project
├── accounts <- ログイン用アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── login.html
│   ├── urls.py
│   └── views.py
├── sample_app <- 固有アプリケーション
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── templates
│   │   └── index.html
│   ├── urls.py
│   └── views.py
├── config
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
```

ログイン用アプリケーションのurls.pyにURLとビューの紐づけを追記する。

```python:accounts/urls.py
urlpatterns =[
    path('login/', views.Login.as_view(), name='login'),
]
```


#リダイレクトURLの設定
setting.pyに未ログイン時に表示されるURL、ログイン・ログアウト時のリダイレクトURLを設定する。

```python:setting.py
LOGIN_URL = '/login' # ログインしていないときのリダイレクト先
LOGIN_REDIRECT_URL = '/sample_app/index' # ログイン後のリダイレクト先
LOGOUT_REDIRECT_URL = '/login' # ログアウト後のリダイレクト先
```

# ログイン認証を追加するアプリケーションの設定
ログイン先のアプリケーションのビューにログイン必須設定を行う。
クラスベースのビューの場合、LoginRequiredMixinをimportして継承することでログイン必須となる。

```python:sample_app/view.py
from django.contrib.auth.mixins import LoginRequiredMixin

class Test(LoginRequiredMixin, TemplateView):
    template_name = 'index.html'
```

メソッドベースのビューの場合、@login_requiredのアノテーションを付与することでログイン必須となる。

```python
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
 
@login_required 
def index(request):
    return render(request, 'index.html')
```

#ユーザーの作成
プロジェクト配下でユーザーを作成する。

```
$ python manage.py createsuperuser
```

#テスト
これでアプリケーションを起動してログインURLにアクセスするとログインフォームが表示されるはずなので、作成したユーザーのID/Passでログインできればログイン認証成功です。