Django ログイン認証後にログイン認証前に要求した URL へのリダイレクトを実装する

シェアする

Django ログイン認証後にログイン認証前に要求した URL へのリダイレクトを実装します。

Django でログイン認証が必要なページを実装する方法として、大きく2つあると思います。

1つは、Django のデコレータ login_required を使う方法です。一番簡単な方法ですが、ページ数が増えてくると、毎回デコレータをつけるのが、大変になってきます。

2つ目は、ミドルウェアを使う方法です。この方法は、Stack Overflow で見つけたのですが、便利なので、よく使っています。

しかし、2つ目の方法で実装した場合、1つ目の Django デコレータで実装していた時にできていた、認証後に、認証前に要求した URL へのリダイレクト機能が動かなくなります。(Get パラメータの next= がつくやつ)

この記事は、そんな私の方のように、ミドルウェアで実装して、next= がなくなった人に向けての記事です。

実装イメージ

  1. http://192.168.33.10:8000/page/ にアクセス
  2. 認証が必要なので、ログインページへリダイレクト
  3. ログイン後、1 でアクセスしたページにリダイレクト

環境とバージョン

  • CentOS 7.5.1804
  • Python 3.6.4
  • Django 2.1.1

インストール

それでは、環境を作っていきます。

Python

$ sudo yum install https://centos7.iuscommunity.org/ius-release.rpm
$ sudo yum install python36u python36u-libs python36u-devel python36u-pip

Python 3.6 をインストールします。

Django

$ sudo pip install --upgrade django

次に Django をインストールします。

実装

環境の次は、実装に入ります。

settings.py

...
ALLOWED_HOSTS = ['*']
INSTALLED_APPS = [
    ...
    'app.apps.AppConfig',
]
MIDDLEWARE = [
    ...
    'login.middleware.LoginRequiredMiddleware',
]
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = LOGIN_URL
...

まず、settings.py です。MIDDLEWARE にこれから自作するミドルウェアのパスを追加します。

middleware.py

from django.http import HttpResponseRedirect
from django.conf import settings
from re import compile

from django.utils.deprecation import MiddlewareMixin

EXEMPT_URLS = [compile(settings.LOGIN_URL.lstrip('/'))]
if hasattr(settings, 'LOGIN_EXEMPT_URLS'):
    EXEMPT_URLS += [compile(expr) for expr in settings.LOGIN_EXEMPT_URLS]

class LoginRequiredMiddleware(MiddlewareMixin):
    def process_request(self, request):
        if not request.user.is_authenticated:
            path = request.path_info.lstrip('/')
            if not any(m.match(path) for m in EXEMPT_URLS):
                if request.path:
                    return HttpResponseRedirect(settings.LOGIN_URL + '?next=' + request.path)
                else:
                    return HttpResponseRedirect(settings.LOGIN_URL)

Stack Overflow を参考に実装しています。settings.py の LOGIN_EXEMPT_URLS に認証が不要な URL を設定し、ログイン URL にリダイレクトします。

この Stack Overflow 通りに実装すると、view 毎に login_required のデコレータを付けなくても良くなり、便利です。しかし、next= が動かなくなってしまうので、処理の最後の方で、request.path があれば、next= をつけるところが今回のポイントとなります。

urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from django.views.generic import TemplateView

urlpatterns = [
    path('', TemplateView.as_view(template_name='app/top.html'), name='top'),
    path('page/', TemplateView.as_view(template_name='app/page.html'), name='page'),

    path('login/', auth_views.LoginView.as_view(template_name='app/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
]

urls.py です。特に説明はありません。

login.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Login</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstr\
ap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"\
 crossorigin="anonymous">

    <!-- Custom styles for this template -->
    <link href="https://getbootstrap.com/docs/4.1/examples/sign-in/signin.css" rel="stylesheet"\
>
  </head>
  <body class="text-center">
    <form class="form-signin" method="post">{% csrf_token %}
      {{ form.errors }}
      <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
      <label for="inputEmail" class="sr-only">Email address</label>
      <input type="email" id="inputEmail" name="username" class="form-control" placeholder="Ema\
il address" required autofocus>
      <label for="inputPassword" class="sr-only">Password</label>
      <input type="password" id="inputPassword" name="password" class="form-control" placeholde\
r="Password" required>
      <input type="hidden" name="next" value="{{ request.GET.next }}">
      <div class="checkbox mb-3">
        <label>
          <input type="checkbox" value="remember-me"> Remember me
        </label>
      </div>
      <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      <p class="mt-5 mb-3 text-muted">&copy; 2017-2018</p>
    </form>
  </body>
</html>

Bootstrap のサンプルをベースにログイン HTML を作成しました。先ほど、ミドルウェアで next= の処理がポイントと言いましたが、ここでもポイントがあります。

ここでさらに、input hidden で next を指定しないといけません。

これで、login_required デコレータと同じ動きになります。

top.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>TOP</title>

    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstr\
ap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"\
 crossorigin="anonymous">
  </head>
  <body>
    <main role="main" class="container">
      <h1 class="mt-5">Top</h1>
      <a class="btn btn-link" href="{% url 'logout' %}">logout</a>
    </main>
  </body>
</html>

ログイン後にリダイレクトする用のトップページを適当に作成します。

page.html

<html>
ページ
</html>

もう一つ任意のページを適当に作成します。

動作確認

実装が終わりましたので、動作を確認していきます。

DB マイグレーション

$ python3.6 manage.py migrate

ログイン認証用のテーブルが必要なので、DB マイグレーションをします。

ログインアカウントの作成

$ python3.6 manage.py createsuperuser

createsuperuser コマンドで、ログインアカウントを作成します。

デバッグサーバの起動

$ python3.6 manage.py runserver 0.0.0.0:8000

Django デバッグサーバの runserver を起動し、http://IPアドレス:8000/page/ にアクセスします。すると、ログインページにリダイレクトされ、先ほど作成したアカウントでログインすると、/page/ にリダイレクトすることが確認できると思います。

また、ログインページに直でアクセスし、ログインすると、settings.py で設定した LOGIN_REDIRECT_URL にリダイレクトします。

まとめ

Django ログイン認証後にログイン認証前に要求した URL へのリダイレクトを実装しました。

私と同じようにミドルウェアで login_required の代わりを実装し、next ができなくなった人が、この記事を読んで少しでも役に立ってもらえれば、とても嬉しいです。

シェアする

フォローする