【Pyhon/Django】ログイン機能カスタマイズ

最近はもっぱらDjangoでのサイト構築の感触を探りながら開発しています(調べる時間の方が圧倒的に多いですが)
当ブログでもDjangoでの機能追加やカスタマイズについて投稿しております。
その中で、先日の記事【Pyhon/Django】カスタムユーザとメールアドレスログイン1について
開発していく上で修正が必要な部分ありましたので、こちらに記載していきます。

修正が必要な部分

ログイン機能の中でログイン後のリダイレクト先を指定しているのですが
パスを固定値にしているためにログインが必要な領域にアクセスした際に
『ログイン画面⇒元のページに戻る』という制御ができません。

かっこ悪いので修正します。

前回のコードおさらい

対象となるのはviews.py部分になります。
ログイン処理の該当部分のみ抜粋しています。

class LoginView(View):
    def post(self, request, *args, **kwargs):
        template_name = 'login.html'
        success = 'home'
        form = LoginForm(data=request.POST)
        if form.is_valid():
            # フォームから'email'を読み取る
            email = form.cleaned_data.get('email')
            # フォームから'password'を読み取る
            password = form.cleaned_data.get('password')
            auth_object = EmailAuthBackend()
            user = auth_object.authenticate(email=email, password=password)
            if user is not None:
                #認証処理
                login(request, user, backend='user.auth.EmailAuthBackend')
                return redirect(success)
            else:
                return render(request, template_name, {'context': 'ログインに失敗しました'})

    def get(self, request, *args, **kwargs):
        template_name = 'login.html'
        form = LoginForm(request.POST)
        return render(request, template_name, {'context': 'get'})
成功した場合にsuccessで指定した"home"に必ずリダイレクトしているため 元々アクセスしようとしていたURLの情報がなくなってしまっています。

修正後のソース

views.pyとlogin.htmlの2つのファイルを修正します。
ますはviews.py
class LoginView(View):
    def post(self, request, *args, **kwargs):
        template_name = 'login.html'
        success = 'home'
        form = LoginForm(data=request.POST)
        if form.is_valid():
            # フォームから'email'を読み取る
            email = form.cleaned_data.get('email')
            # フォームから'password'を読み取る
            password = form.cleaned_data.get('password')
            auth_object = EmailAuthBackend()
            user = auth_object.authenticate(email=email, password=password)
            if user is not None:
                #認証処理
                login(request, user, backend='user.auth.EmailAuthBackend')
                redirect_to = request.POST.get('redirect_to')  #1 redirect_toの取得
                if redirect_to is not None:
                    return redirect(redirect_to)
                else:
                    return redirect(success)
            else:
                return render(request, template_name, {'context': 'ログインに失敗しました'})
    def get(self, request, *args, **kwargs):
        template_name = 'login.html'
        form = LoginForm(request.POST)
        redirect_to = request.GET.get('next')  #2 getパラメータで受け取ったnextをtemplateへ渡す
        return render(request, template_name, {'context': 'get', 'redirect_to': redirect_to})
まず#1の部分ですが、postパラメータでログイン後の遷移先を受け取っています。 もし指定されていなければ、共通のリダイレクト先(home)に遷移するようにします。 パラメータを受け取っていますので、htmlのformから「redirect_to」という値を 渡す必要があります。 #2はgetパラメータから「next」という値を取得しています。 「next」というパラメータはDjangoの認証機能に組み込まれており ログインが必要な領域にアクセスした際にgetパラメータに勝手に「next」というパラメータ名で アクセスしようとしていたパスが格納されます。 この「next」の値を受け取り、login.htmlに渡します。 それでは次にlogin.htmlの修正です。
<body class="text-center">
<main class="form-signin">
  <form method="post">{% csrf_token %}
    <img class="mb-4" src="/docs/5.0/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57">
    <h1 class="h3 mb-3 fw-normal">ログイン</h1>
      {{ context }}
    <label for="inputEmail" class="visually-hidden">メールアドレス</label>
    <input type="email" id="inputEmail" class="form-control" placeholder="メールアドレス" name = "email" required autofocus>
    <label for="inputPassword" class="visually-hidden">パスワード</label>
    <input type="password" id="inputPassword" class="form-control" placeholder="パスワード" name = "password" required>
    {% if redirect_to is not None %} <!-#3-修正点ここから-->
      <input type="hidden" name="redirect_to" value="{{ redirect_to }}">
    {% endif %}  <!-#3-修正点ここまで-->
    <div class="checkbox mb-3">
      <label>
        <input type="checkbox" value="remember-me"> 記憶する
      </label>
    </div>
    <button class="w-100 btn btn-lg btn-primary" type="submit">ログイン</button>
    <p class="mt-5 mb-3 text-muted">&amp;copy; 2017-2021</p>
  </form>
</main>
</body>
修正点は11行目の追加飲みです。 views.pyから渡された「redirect_to 」の値をhiddenに埋め込んで、再度views.pyに渡してやります。 こうすることで、ログインが成功した場合に元々アクセスしようとしていたページにリダイレクトすることができます。

課題

ログイン後のリダイレクト先はgetパラメータに指定した値を利用しています。
そのため、URLを書き換えることで
ログイン後に他のサイトにアクセスさせることもできてしまいます。
これを悪用されると、例えば下記のようなURLをどこかに掲載されてしまうと
ログイン後に勝手にyahooトップに飛ばすこともできてしまいます。
https://ドメイン/?next=https://yahoo.co.jp

遷移先がyahooトップならまだいいのですが、この遷移先をフィッシングサイトにして
悪用されることがあります。
このようなセキュリティ対策のため、リダイレクト先のURLをパラメータから取得して利用する場合は
ドメインをホワイトリストとして管理するか、相対パスしか受け付けないように制御を入れる必要があります。

その他の記事

Python
【Pyhon/Django】ログイン機能カスタマイズ
Python
Django
【Pyhon/Django】カスタムユーザとメールアドレスログイン1
Python
django
Python/Django 遭遇したエラー集と解決方法
Django
django
Djangoでadminにカスタムフィルタ追加

コメントを残す