Djangoでaccountsアプリケーションを使わずにログイン認証する

DjangoでWebサイトを作る際に、**accountsアプリケーションを作成することなくログイン認証をする**必要に迫られたのですが、包括的、体系的に説明しているサイトに出会えなかったため書き残します。

# 環境
- Python 3.7.0
- Django 2.1.4

(2020/3月追記: Python 3.7.4, Django 3.0でも動作確認済みです)

# ディレクトリ構成
ディレクトリは以下のように構成されています。ここでは、ログイン認証に必要な最低限のものを書きます。

mysite/
　├ manage.py
　├ mysite/
　│　　├ settings.py
　│　　└ urls.py
　└ myapp/
　　　　├ urls.py
　　　　├ views.py
　　　　├ forms.py
　　　　├ models.py
　　　　└ templates/
　　　　　　├ base.html
　　　　　　└ myapp/
　　　　　　　　├ mymodel_detail.html
　　　　　　　　├ signup.html
　　　　　　　　└ などいろいろなhtml

よくあるログイン認証の紹介では、accountsアプリケーションを使うために、

```BASH:
$ python manage.py startapp accounts
```
を用いて、`mysite/accounts/`を作成、`mysite/mysite/settings.py`のINSTALLED_APPSに`'accounts.apps.AccountsConfig'`を追加する、とあると思いますが、今回は**INSTALLED_APPSにaccountsアプリケーションを追加せずにログイン認証をします**。

なお、今回の場合でも**ログイン認証だけを考えるならば**、`mysite/mysite/models.py`の変更は不要です。

# 前提となる設定

まず、`mysite/mysite/settings.py`に**myappをアプリケーションとして追加しています**。

```Python:mysite/mysite/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myapp' # Here
]
```

また、ルートのURLとして、`myapp`を登録しています。

```Python:mysite/mysite/settings.py
ROOT_URLCONF = 'myapp.urls'
```

さらに、**ログインしなければログイン関係やサインアップ関係のページ以外は一切見れないようにします**（設定方法は後述）。

# 必要な設定
最初に必要な設定をまとめると、次のようになります。

- `mysite/mysite/settings.py`にログインやログアウトのためのURLとそれぞれのリダイレクト先を明記
- `mysite/myapp/urls.py`にログイン、ログアウトなどのURLを登録
- `mysite/myapp/views.py`に**必要ならば**ログイン、ログアウトなどを登録、さらにログイン時以外に見れないようにする
- `mysite/myapp/templates/`にログインなどのhtmlを作成


# `mysite/mysite/settings.py`の設定

以下では、名前付きURLパターンを使います。これにより、アプリケーションごとに名前空間の分離を図ることができます。**`myapp`をアプリケーションとして`settings.py`で登録していることに注意してください**。

名前付きURLパターンの公式のマニュアル
https://docs.djangoproject.com/ja/2.1/topics/http/urls/#naming-url-patterns

django.contrib.auth、つまりログインなどに関するURLの公式のマニュアル
https://docs.djangoproject.com/ja/2.1/ref/settings/#auth

## ログイン、ログアウトのURLの設定
ログイン、ログアウトのURLに用いるURLは、通常はaccountsアプリケーションの存在を仮定しています。この設定のデフォルトでは、

```Python:
LOGIN_URL='accounts/login/'     # ログイン
LOGOUT_URL='accounts/logout/'   # ログアウト
```

とされているため、今回は、

```Python:mysite/mysite/settings.py
LOGIN_URL='myapp:login'     # ログイン
LOGOUT_URL='myapp:logout'   # ログアウト
```

と設定します。`myapp:login`などとしているのは名前付きURLパターンを用いているためです。ここで、 `login` や `logout` は `myapp.views` のなかで、ログイン、ログアウトのURLを `name='login'` などと名前の登録をすることで使えるようになり、この機能と同じ `myapp.views` 内の `app_name='myapp'` の登録により、URLを変更したとしても名前で追うことができ、ハードコーディングをしないでいいようになります。（これらの登録も後の項目で載せます）

## リダイレクト先の設定
ログイン、ログアウトが完了した後に遷移するURL（リダイレクト先）を登録する必要もあります。デフォルト値も併せて紹介すると、

```Python:
LOGIN_REDIRECT_URL='/accounts/profile/' # ログイン
LOGOUT_REDIRECT_URL='None'              # ログアウト
```

です。例えば、ログインした後に遷移するURLを`myapp/home`、ログアウトした後に遷移するURLを`myapp/login`にする（ログインページにする）ならば、以下のように設定します。

```Python:mysite/mysite/settings.py
LOGIN_REDIRECT_URL='myapp:home'      # ログイン
LOGOUT_REDIRECT_URL='myapp:login'    # ログアウト
```

このようにリダイレクト先を設定しておけば、ログインやログアウトが完了すると自動的に画面が遷移します。

## `mysite/myapp/templates/`の存在を明記すべきかどうか
```Python:mysite/mysite/settings.py
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
```

で

```Python:
'DIRS': [os.path.join(BASE_DIR, 'templates')], 
```
とすべきと書いてあるページもありますが、今回はなくても大丈夫です。（もしもだめだったら追加してください）



# `mysite/myapp/urls.py`にログイン、ログアウトなどを登録

作成したものは以下のものです。

```Python:mysite/myapp/urls.py
from django.urls import path, include

from . import views # mysite/myapp/views.pyをインポート

from django.contrib.auth import views as auth_views

app_name = 'myapp'
urlpatterns = [
    path('home/', views.home, name='home'),
    path('login/', auth_views.LoginView.as_view(template_name='myapp/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('signup/', views.signup, name='signup'),
    path('password_change/', auth_views.PasswordChangeView.as_view(), name='password_change'),
    path('password_change/done/', auth_views.PasswordChangeDoneView.as_view(), name='password_change_done'),
#     path('password_reset/', auth_views.PasswordResetView.as_view(template_name='myapp/password_reset.html'), name='password_reset'),
#     path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
#     path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
#     path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
```

ここでは、パスワード変更などのページについてはすべてコメントアウトしていますが、これらを用いれば他の機能も拡張できます。また、アプリケーションとして`myapp`を用いるため、`app_name = 'myapp'`と書いていることに注意してください。

また、**サインアップに関するビューはデフォルトでないですが、それ以外は`django.contrib.auth`を使うことができます**。
accountsアプリケーションを用いる場合には一括で`accounts/`下に一括で利用することができるようになっていますが、accountsアプリケーションを利用せずとも、個別に上に載せたものは上記のように利用することができます。

ログインなどに関するビューもデフォルトのままでよいなら自分で`mysite/myapp/views.py`に登録する必要もありません（個別にカスタマイズしたいならこれらのクラスを継承したものを作成する必要があります）。


ここで、ログインのみ引数を与えましたが、これはテンプレートの存在場所を指定するためです。ログインやログアウトのhtmlは、デフォルトでは以下の通りです。

- ログイン画面: templates/registration/login.html
- ログアウト画面: templates/registration/logged_out.html

これは、上記のコードのように`as_view`の引数に`template_name=(mysite/myapp/templates/以下の場所の指定)`と指定することで変更することができます。

# `mysite/myapp/views.py`

## 関数の作成
ログインやログアウトなど、`django.contrib.auth`のままでよいならば書く必要はありませんが、メールアドレス認証など、独自のクラスが必要ならば作成する必要がありますが、いらないならば特になにもしなくてよいです。

しかし、**サインアップに関するビューはないため、自分で作る必要があります**。
例えば、次のようにして作成できます。

```Python:mysite/myapp/views.py
from django.contrib.auth.forms import UserCreationForm # ユーザー作成のため追加

def signup(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            return redirect('myapp/main')
    else:
        form = UserCreationForm()
    return render(request, 'myapp/signup.html', {'form': form})
```

## ログインしていないときに見れないようにする
ログインしていないときにページを見れないようにするためには、`mysite/myapp/views.py`の途中で、

```Python:mysite/myapp/views.py
from django.contrib.auth.decorators import login_required

# ログインしていないときにも見れるページに関する関数
# 例えば、def signup(request)など

@login_required

# ログインしているときのみ見れるページに関する関数
# 例えば、def main(request)など

```

を書くとよいです。クラスを用いているときには、

```Python:views.py
from django.contrib.auth.mixins import LoginRequiredMixin

# ログインしているときのみ見れるページのクラスベースのビュー
def myclass(LoginRequiredMixin, ...):
    ...
```

のように、**最初に`LoginRequiredMixin`を継承します**。

# `mysite/myapp/templates/`にログインなどのhtmlを作成

ログインのためのhtmlを作成します。
詳細は省略しますが、

```html:mysite/myapp/templates/myapp/login.html
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
```

のように、`django.contrib.auth`の`LoginView`を用いているならば、`{{ form.username }}`と`{{ form.password }}`でユーザー名とパスワードを利用でき、

```html:mysite/myapp/templates/myapp/login.html
<input type="submit" id="login_button" class="login_button" name="login_button" value="ログイン" onclick="location.href='./../home'" />
<input name="next" type="hidden" value="{{ next }}"/>

```

などとすれば、ログインボタンが作成できます。

これで、ログインができるようになりました。

なお、logout用のhtmlファイルはログアウト後のページを作らないならば（今回の場合はログイン画面に遷移させているので）不要です。


# まとめ
みなさん快適なDjangoライフを！