Djangoリダイレクトの究極のガイド

Djangoリダイレクトの究極のガイド

Django frameworkを使用してPython Webアプリケーションを構築する場合、ある時点でユーザーをあるURLから別のURLにリダイレクトする必要があります。

このガイドでは、HTTPリダイレクトとDjangoでそれらを処理する方法について知る必要があるすべてを学びます。 このチュートリアルの最後では、次のことを行います。

  • ユーザーをあるURLから別のURLにリダイレクトできる

  • 一時的なリダイレクトと永続的なリダイレクトの違いを知る

  • リダイレクトを操作するときによくある落とし穴を避ける

このチュートリアルは、viewsURL patternsなどのDjangoアプリケーションの基本的な構成要素に精通していることを前提としています。

Djangoリダイレクト:非常に簡単な例

Djangoでは、ビューからHttpResponseRedirectまたはHttpResponsePermanentRedirectのインスタンスを返すことにより、ユーザーを別のURLにリダイレクトします。 これを行う最も簡単な方法は、モジュールdjango.shortcutsの関数redirect()を使用することです。 例を示しましょう。

# views.py
from django.shortcuts import redirect

def redirect_view(request):
    response = redirect('/redirect-success/')
    return response

ビュー内のURLを使用してredirect()を呼び出すだけです。 HttpResponseRedirectクラスが返され、ビューから返されます。

他のビューと同様に、リダイレクトを返すビューをurls.pyに追加する必要があります。

# urls.py
from django.urls import path

from .views import redirect_view

urlpatterns = [
    path('/redirect/', redirect_view)
    # ... more URL patterns here
]

これがDjangoプロジェクトのメインのurls.pyであるとすると、URL/redirect//redirect-success/にリダイレクトされます。

URLのハードコーディングを回避するには、ビュー、URLパターン、またはモデルの名前を使用してredirect()を呼び出し、リダイレクトURLのハードコーディングを回避します。 キーワード引数permanent=Trueを渡すことにより、永続的なリダイレクトを作成することもできます。

この記事はここで終了する可能性がありますが、「Djangoリダイレクトの究極のガイド」とは言い難いものです。 redirect()関数をすぐに詳しく見て、HTTPステータスコードとさまざまなHttpRedirectResponseクラスの詳細についても説明しますが、一歩下がって基本的なことから始めましょう。質問。

リダイレクトする理由

そもそもなぜユーザーを別のURLにリダイレクトしたいのか疑問に思うかもしれません。 リダイレクトの意味を理解するには、フレームワークがデフォルトで提供する機能にDjango自体がどのようにリダイレクトを組み込むかを見てください。

  • ログインしていない場合、Django管理者など、認証が必要なURLを要求すると、Djangoはログインページにリダイレクトします。

  • ログインに成功すると、Djangoは最初にリクエストしたURLにリダイレクトします。

  • Django adminを使用してパスワードを変更すると、変更が成功したことを示すページにリダイレクトされます。

  • Django adminでオブジェクトを作成すると、Djangoはオブジェクトリストにリダイレクトします。

リダイレクトなしの代替実装はどのようになりますか? ユーザーがログインしてページを表示する必要がある場合は、「ここをクリックしてログインします」などのページを表示するだけで済みます。これは機能しますが、ユーザーにとっては不便です。

http://bit.lyのようなURL短縮サービスは、リダイレクトが役立つもう1つの例です。ブラウザのアドレスバーに短いURLを入力すると、長くて扱いにくいURLのページにリダイレクトされます。

それ以外の場合、リダイレクトは単に便利な問題ではありません。 リダイレクトは、Webアプリケーションを介してユーザーを導くための不可欠な手段です。 オブジェクトの作成や削除など、副作用を伴う何らかの操作を実行した後、別のURLにリダイレクトして、誤って2回操作を実行しないようにすることをお勧めします。

このリダイレクトの使用例の1つにフォーム処理があります。この場合、ユーザーはフォームを正常に送信した後、別のURLにリダイレクトされます。 以下は、通常フォームをどのように処理するかを示すコードサンプルです。

 1 from django import forms
 2 from django.http import HttpResponseRedirect
 3 from django.shortcuts import redirect, render
 4
 5 def send_message(name, message):
 6     # Code for actually sending the message goes here
 7
 8 class ContactForm(forms.Form):
 9     name = forms.CharField()
10     message = forms.CharField(widget=forms.Textarea)
11
12 def contact_view(request):
13     # The request method 'POST' indicates
14     # that the form was submitted
15     if request.method == 'POST':  # 1
16         # Create a form instance with the submitted data
17         form = ContactForm(request.POST)  # 2
18         # Validate the form
19         if form.is_valid():  # 3
20             # If the form is valid, perform some kind of
21             # operation, for example sending a message
22             send_message(
23                 form.cleaned_data['name'],
24                 form.cleaned_data['message']
25             )
26             # After the operation was successful,
27             # redirect to some other page
28             return redirect('/success/')  # 4
29     else:  # 5
30         # Create an empty form instance
31         form = ContactForm()
32
33     return render(request, 'contact_form.html', {'form': form})

このビューの目的は、ユーザーがメッセージを送信できる連絡先フォームを表示および処理することです。 手順を追って説明しましょう。

  1. 最初に、ビューは要求メソッドを調べます。 ユーザーがこのビューに接続されているURLにアクセスすると、ブラウザーはGET要求を実行します。

  2. ビューがPOST要求で呼び出された場合、POSTデータはContactFormオブジェクトをインスタンス化するために使用されます。

  3. フォームが有効な場合、フォームデータはsend_message()に渡されます。 この関数はこのコンテキストには関係ないため、ここでは示しません。

  4. メッセージを送信した後、ビューはURL/success/へのリダイレクトを返します。 これは私たちが興味を持っているステップです。 簡単にするために、ここではURLがハードコーディングされています。 後でそれを回避する方法については後で説明します。

  5. ビューがGETリクエスト(正確には、POSTリクエストではないあらゆる種類のリクエスト)を受信すると、ContactFormのインスタンスを作成し、django.shortcuts.render()を使用します。 scontact_form.htmlテンプレートをレンダリングします。

ユーザーがリロードを押すと、/success/のURLのみがリロードされます。 リダイレクトがなければ、ページをリロードするとフォームが再送信され、別のメッセージが送信されます。

舞台裏:HTTPリダイレクトの仕組み

リダイレクトが理にかなっている理由はわかりましたが、どのように機能しますか? WebブラウザーのアドレスバーにURLを入力するとどうなるかを簡単に説明しましょう。

HTTPのクイックプライマー

パス/hello/を処理する「Hello World」ビューを備えたDjangoアプリケーションを作成したと仮定します。 Django開発サーバーでアプリケーションを実行しているため、完全なURLはhttp://127.0.0.1:8000/hello/です。

ブラウザにそのURLを入力すると、サーバーのポート8000にIPアドレス127.0.0.1で接続し、パス/hello/に対してHTTPGET要求を送信します。 サーバーはHTTP応答で応答します。

HTTPはテキストベースであるため、クライアントとサーバーの間を行き来するのは比較的簡単です。 コマンドラインツールcurlとオプション--includeを使用して、次のようにヘッダーを含む完全なHTTP応答を確認できます。

$ curl --include http://127.0.0.1:8000/hello/
HTTP/1.1 200 OK
Date: Sun, 01 Jul 2018 20:32:55 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Content-Type: text/html; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 11

Hello World

ご覧のとおり、HTTP応答は、ステータスコードとステータスメッセージを含むステータス行で始まります。 ステータス行の後には、任意の数のHTTPヘッダーが続きます。 空の行は、ヘッダーの終わりと応答本文の始まりを示し、サーバーが送信する実際のデータが含まれます。

HTTPリダイレクトのステータスコード

リダイレクト応答はどのように見えますか? 前に示したように、パス/redirect/redirect_view()によって処理されると仮定します。 curlを使用してhttp://127.0.0.1:8000/redirect/にアクセスすると、コンソールは次のようになります。

$ curl --include http://127.0.0.1:8000/redirect/
HTTP/1.1 302 Found
Date: Sun, 01 Jul 2018 20:35:34 GMT
Server: WSGIServer/0.2 CPython/3.6.3
Content-Type: text/html; charset=utf-8
Location: /redirect-success/
X-Frame-Options: SAMEORIGIN
Content-Length: 0

2つの応答は似ているように見えますが、いくつかの重要な違いがあります。 リダイレクト:

  • 異なるステータスコードを返します(302200

  • 相対URLを含むLocationヘッダーが含まれています

  • リダイレクト応答の本文が空であるため、空行で終了します

主な差別化要因はステータスコードです。 HTTP標準の仕様には、次のことが記載されています。

302(Found)ステータスコードは、ターゲットリソースが一時的に別のURIの下にあることを示します。 リダイレクションは時々変更されるかもしれないので、クライアントは将来のリクエストに有効なリクエストURIを使い続けるべきです。 サーバーは、異なるURIのURI参照を含む応答にLocationヘッダーフィールドを生成する必要があります。 ユーザーエージェントは、自動リダイレクトにロケーションフィールド値を使用してもよい[MAY]。 (Source

つまり、サーバーが302のステータスコードを送信するたびに、クライアントに「ねえ、現時点では、探しているものはこの別の場所にあります」と表示されます。

仕様のキーフレーズは、「自動リダイレクトにLocationフィールドの値を使用できます」です。つまり、クライアントに別のURLを強制的にロードさせることはできません。 クライアントは、ユーザーの確認を待つか、URLをまったくロードしないかを選択できます。

これで、リダイレクトは3xxステータスコードとLocationヘッダーを持つ単なるHTTP応答であることがわかりました。 ここで重要なポイントは、HTTPリダイレクトは古いHTTP応答と似ていますが、本文が空で、3xxステータスコードがあり、Locationヘッダーがあることです。

それでおしまい。 これをDjangoに一時的に結び付けますが、最初に、その3xxのステータスコード範囲内の2種類のリダイレクトを見て、Web開発に関してそれらが重要である理由を確認しましょう。

一時的vs. 永続的なリダイレクト

HTTP標準では、すべて3xxの範囲のいくつかのリダイレクトステータスコードが指定されています。 最も一般的な2つのステータスコードは301 Permanent Redirect302 Foundです。

ステータスコード302 Foundは、一時的なリダイレクトを示します。 一時的なリダイレクトでは、「現時点では、探しているものはこの別のアドレスで見つけることができます。」 「店舗は現在改装工事のため閉鎖されています。 角を曲がったところにある他の店に行ってください。」これは一時的なものであるため、次に買い物をするときに元の住所を確認します。

Note: HTTP 1.0では、ステータスコード302のメッセージはTemporary Redirectでした。 HTTP 1.1では、メッセージがFoundに変更されました。

名前が示すように、永続的なリダイレクトは永続的であると想定されています。 永続的なリダイレクトはブラウザに次のように伝えます。「探しているのはこのアドレスではなくなりました。 現在はこの新しい住所にあり、以前の住所に戻ることはありません。」

永続的なリダイレクトは、「移動しました。 新しい店がすぐ近くにあります。」この変更は永続的なものであるため、次回店舗に行きたいときには、新しい住所に直行します。

Note:永続的なリダイレクトは、意図しない結果をもたらす可能性があります。 永続的なリダイレクトを使用する前にこのガイドを終了するか、「永続的なリダイレクトは永続的です」セクションに直接ジャンプしてください。

ブラウザは、リダイレクトを処理するときも同様に動作します。URLが永続的なリダイレクト応答を返すと、この応答はキャッシュされます。 ブラウザが次に古いURLに遭遇すると、リダイレクトを記憶し、新しいアドレスを直接要求します。

リダイレクトをキャッシュすると、不要なリクエストが保存され、ユーザーエクスペリエンスが向上し、高速になります。

さらに、一時リダイレクトと永続リダイレクトの区別は、検索エンジン最適化に関連しています。

Djangoのリダイレクト

これで、リダイレクトは3xxステータスコードとLocationヘッダーを持つ単なるHTTP応答であることがわかりました。

このような応答は、通常のHttpResponseオブジェクトから自分で作成できます。

def hand_crafted_redirect_view(request):
  response = HttpResponse(status=302)
  response['Location'] = '/redirect/success/'
  return response

このソリューションは技術的には正しいものですが、かなりの入力が必要です。

HTTPResponseRedirectクラス

HttpResponseのサブクラスであるクラスHttpResponseRedirectを使用すると、入力を節約できます。 リダイレクトするURLを最初の引数としてクラスをインスタンス化するだけで、クラスは正しいステータスとLocationヘッダーを設定します:

def redirect_view(request):
  return HttpResponseRedirect('/redirect/success/')

PythonシェルのHttpResponseRedirectクラスで遊んで、何が得られるかを確認できます。

>>>

>>> from django.http import HttpResponseRedirect
>>> redirect = HttpResponseRedirect('/redirect/success/')
>>> redirect.status_code
302
>>> redirect['Location']
'/redirect/success/'

永続的なリダイレクト用のクラスもあり、これは適切にHttpResponsePermanentRedirectという名前です。 HttpResponseRedirectと同じように機能しますが、唯一の違いは、ステータスコードが301 (Moved Permanently)であるということです。

Note:上記の例では、リダイレクトURLはハードコーディングされています。 URLをハードコーディングすることは悪い習慣です。URLが変更された場合、すべてのコードを検索して、出現箇所を変更する必要があります。 それを修正しましょう!

django.urls.reverse()を使用してURLを作成することもできますが、次のセクションで説明するように、より便利な方法があります。

redirect()関数

あなたの生活を楽にするために、Djangoはあなたがすでに紹介で見た多目的なショートカット機能django.shortcuts.redirect()を提供します。

この関数を呼び出すには:

  • get_absolute_url()メソッドを使用したモデルインスタンスまたはその他のオブジェクト

  • URLまたはビュー名と位置引数および/またはキーワード引数

  • URL

引数をURLに変換し、HTTPResponseRedirectを返すために適切な手順を実行します。 permanent=Trueを渡すと、HttpResponsePermanentRedirectのインスタンスが返され、永続的なリダイレクトが発生します。

さまざまなユースケースを説明する3つの例を次に示します。

  1. モデルを渡す:

    from django.shortcuts import redirect
    
    def model_redirect_view(request):
        product = Product.objects.filter(featured=True).first()
        return redirect(product)

    redirect()product.get_absolute_url()を呼び出し、その結果をリダイレクトターゲットとして使用します。 指定されたクラス(この場合はProduct)にget_absolute_url()メソッドがない場合、これはTypeErrorで失敗します。

  2. URL名と引数を渡す:

    from django.shortcuts import redirect
    
    def fixed_featured_product_view(request):
        ...
        product_id = settings.FEATURED_PRODUCT_ID
        return redirect('product_detail', product_id=product_id)

    redirect()は、指定された引数を使用してURLを逆にしようとします。 この例では、URLパターンに次のようなパターンが含まれていることを前提としています。

    path('/product//', 'product_detail_view', name='product_detail')
  3. URLを渡す:

    from django.shortcuts import redirect
    
    def featured_product_view(request):
        return redirect('/products/42/')

    redirect()は、/または.を含むすべての文字列をURLとして扱い、リダイレクトターゲットとして使用します。

RedirectViewクラスベースビュー

リダイレクトを返すだけのビューがある場合は、クラスベースのビューdjango.views.generic.base.RedirectViewを使用できます。

さまざまな属性を使用して、ニーズに合わせてRedirectViewを調整できます。

クラスに.url属性がある場合、リダイレクトURLとして使用されます。 文字列フォーマットのプレースホルダーは、URLの名前付き引数に置き換えられます。

# urls.py
from django.urls import path
from .views import SearchRedirectView

urlpatterns = [
    path('/search//', SearchRedirectView.as_view())
]

# views.py
from django.views.generic.base import RedirectView

class SearchRedirectView(RedirectView):
  url = 'https://google.com/?q=%(term)s'

URLパターンは引数termを定義します。これは、リダイレクトURLを構築するためにSearchRedirectViewで使用されます。 アプリケーションのパス/search/kittens/は、https://google.com/?q=kittensにリダイレクトします。

RedirectViewをサブクラス化してurl属性を上書きする代わりに、キーワード引数urlurlpatternsas_view()に渡すこともできます。

#urls.py
from django.views.generic.base import RedirectView

urlpatterns = [
    path('/search//',
         RedirectView.as_view(url='https://google.com/?q=%(term)s')),
]

get_redirect_url()を上書きして、完全にカスタムの動作を取得することもできます。

from random import choice
from django.views.generic.base import RedirectView

class RandomAnimalView(RedirectView):

     animal_urls = ['/dog/', '/cat/', '/parrot/']
     is_permanent = True

     def get_redirect_url(*args, **kwargs):
        return choice(self.animal_urls)

このクラスベースのビューは、.animal_urlsからランダムに選択されたURLにリダイレクトされます。

django.views.generic.base.RedirectViewは、カスタマイズ用のフックをさらにいくつか提供します。 これが完全なリストです:

  • .url

    この属性が設定されている場合は、リダイレクト先のURLを含む文字列である必要があります。 %(name)sのような文字列フォーマットのプレースホルダーが含まれている場合、ビューに渡されるキーワード引数を使用して展開されます。

  • .pattern_name

    この属性が設定されている場合は、リダイレクト先のURLパターンの名前である必要があります。 ビューに渡される位置引数およびキーワード引数は、URLパターンを逆にするために使用されます。

  • .permanent

    この属性がTrueの場合、ビューは永続的なリダイレクトを返します。 デフォルトはFalseです。

  • .query_string

    この属性がTrueの場合、ビューは指定されたクエリ文字列をリダイレクトURLに追加します。 デフォルトであるFalseの場合、クエリ文字列は破棄されます。

  • get_redirect_url(*args, **kwargs)

    このメソッドは、リダイレクトURLの構築を担当します。 このメソッドがNoneを返す場合、ビューは410 Goneステータスを返します。

    デフォルトの実装では、最初に.urlがチェックされます。 ビューに渡された名前付きURLパラメーターを使用して、名前付きフォーマット指定子を展開し、.urlを「古いスタイル」のformat stringとして扱います。

    .urlが設定されていない場合は、.pattern_nameが設定されているかどうかを確認します。 存在する場合は、それを使用して、受け取った位置引数およびキーワード引数でURLを反転します。

    このメソッドを上書きすることで、その動作を任意の方法で変更できます。 URLを含む文字列を返すことを確認してください。

Note:クラスベースのビューは強力な概念ですが、頭を包み込むのは少し難しい場合があります。 コードの流れをたどるのが比較的簡単な通常の関数ベースのビューとは異なり、クラスベースのビューは、ミックスインと基本クラスの複雑な階層で構成されています。

クラスベースのビュークラスを理解するための優れたツールは、WebサイトClassy Class-Based Viewsです。

この単純な関数ベースのビューを使用して、上記の例のRandomAnimalViewの機能を実装できます。

from random import choice
from django.shortcuts import redirect

def random_animal_view(request):
    animal_urls = ['/dog/', '/cat/', '/parrot/']
    return redirect(choice(animal_urls))

ご覧のように、クラスベースのアプローチは、隠れた複雑さを追加する一方で、明らかな利点を提供しません。 それは疑問を提起します:いつRedirectViewを使うべきですか?

urls.pyに直接リダイレクトを追加する場合は、RedirectViewを使用するのが理にかなっています。 ただし、get_redirect_urlを上書きしていることに気付いた場合は、関数ベースのビューが理解しやすく、将来の拡張に柔軟に対応できる可能性があります。

高度な使用法

おそらくdjango.shortcuts.redirect()を使用することがわかったら、別のURLにリダイレクトするのは非常に簡単です。 しかし、それほど明白ではない高度なユースケースがいくつかあります。

リダイレクトでパラメーターを渡す

場合によっては、リダイレクト先のビューにいくつかのパラメーターを渡したいことがあります。 最適なオプションは、リダイレクトURLのクエリ文字列でデータを渡すことです。つまり、次のようなURLにリダイレクトします。

http://example.com/redirect-path/?parameter=value

some_view()からproduct_view()にリダイレクトしたいが、オプションのパラメーターcategoryを渡したいとしましょう。

from django.urls import reverse
from urllib.parse import urlencode

def some_view(request):
    ...
    base_url = reverse('product_view')  # 1 /products/
    query_string =  urlencode({'category': category.id})  # 2 category=42
    url = '{}?{}'.format(base_url, query_string)  # 3 /products/?category=42
    return redirect(url)  # 4

def product_view(request):
    category_id = request.GET.get('category')  # 5
    # Do something with category_id

この例のコードは非常に高密度なので、手順を追って説明しましょう。

  1. まず、django.urls.reverse()を使用して、product_view()へのURLマッピングを取得します。

  2. 次に、クエリ文字列を作成する必要があります。 それが疑問符の後の部分です。 特殊文字を適切にエンコードするため、urllib.urlparse.urlencode()を使用することをお勧めします。

  3. ここで、base_urlquery_stringを疑問符で結合する必要があります。 そのためにフォーマット文字列は問題なく機能します。

  4. 最後に、urldjango.shortcuts.redirect()またはリダイレクト応答クラスに渡します。

  5. リダイレクトターゲットであるproduct_view()では、パラメーターはrequest.GETディクショナリで使用可能になります。 パラメータが欠落している可能性があるため、requests.GET['category']ではなくrequests.GET.get('category')を使用する必要があります。 前者はパラメータが存在しない場合にNoneを返しますが、後者は例外を発生させます。

Note:クエリ文字列から読み取ったデータを必ず検証してください。 リダイレクトURLを作成したため、このデータは管理下にあるように思われるかもしれません。

実際には、リダイレクトはユーザーによって操作される可能性があり、他のユーザー入力のように信頼されてはなりません。 適切な検証がない場合、an attacker might be able gain unauthorized access

特別なリダイレクトコード

Djangoは、ステータスコード301および302のHTTP応答クラスを提供します。 これらはほとんどのユースケースをカバーするはずですが、ステータスコード303307、または308を返す必要がある場合は、独自の応答クラスを非常に簡単に作成できます。 HttpResponseRedirectBaseをサブクラス化し、status_code属性を上書きするだけです。

class HttpResponseTemporaryRedirect(HttpResponseRedirectBase):
    status_code = 307

または、django.shortcuts.redirect()メソッドを使用して応答オブジェクトを作成し、戻り値を変更することもできます。 この方法は、ビューまたはURLの名前、またはリダイレクト先のモデルがある場合に意味があります。

def temporary_redirect_view(request):
    response = redirect('success_view')
    response.status_code = 307
    return response

Note:実際には、3xxの範囲のステータスコードを持つ3番目のクラスがあります:HttpResponseNotModified、ステータスコード304。 コンテンツURLが変更されておらず、クライアントがキャッシュバージョンを使用できることを示しています。

304 Not Modifiedの応答がキャッシュされたバージョンのURLにリダイレクトされると主張する人もいるかもしれませんが、それは少し難しいです。 その結果、HTTP標準の“Redirection 3xx” sectionにリストされなくなりました。

落とし穴

リダイレクトしないだけのリダイレクト

django.shortcuts.redirect()の単純さは欺くことができます。 関数自体はリダイレクトを実行しません。リダイレクト応答オブジェクトを返すだけです。 ビューから(またはミドルウェアで)この応答オブジェクトを返す必要があります。 それ以外の場合、リダイレクトは行われません。

ただし、redirect()を呼び出すだけでは不十分であることがわかっている場合でも、単純なリファクタリングによって、このバグを動作中のアプリケーションに簡単に導入できます。 以下に例を示します。

ショップを建設していて、製品を表示するビューを持っていると仮定しましょう。 製品が存在しない場合、ホームページにリダイレクトします:

def product_view(request, product_id):
    try:
        product = Product.objects.get(pk=product_id)
    except Product.DoesNotExist:
        return redirect('/')
    return render(request, 'product_detail.html', {'product': product})

次に、製品の顧客レビューを表示する2番目のビューを追加します。 また、存在しない製品のホームページにリダイレクトする必要があるため、最初のステップとして、この機能をproduct_view()からヘルパー関数get_product_or_redirect()に抽出します。

def get_product_or_redirect(product_id):
    try:
        return Product.objects.get(pk=product_id)
    except Product.DoesNotExist:
        return redirect('/')

def product_view(request, product_id):
    product = get_product_or_redirect(product_id)
    return render(request, 'product_detail.html', {'product': product})

残念ながら、リファクタリング後、リダイレクトは機能しなくなりました。

リダイレクトを停止しないリダイレクト

リダイレクトを処理する場合、URL AがURL Aへのリダイレクトを返すURL Bを指すリダイレクトをURL Aに返すなどして、誤ってリダイレクトループを作成する可能性があります。 ほとんどのHTTPクライアントは、この種類のリダイレクトループを検出し、多数のリクエストの後にエラーメッセージを表示します。

残念ながら、この種のバグは、サーバー側ではすべてが正常に見えるため、見つけるのが難しい場合があります。 ユーザーが問題について不満を言っていない限り、何かが間違っている可能性があることを示す唯一の兆候は、1つのクライアントから多数のリクエストがあり、すべてがすばやく連続してリダイレクト応答を返すが、200 OKの応答がないことです。状態。

リダイレクトループの簡単な例を次に示します。

def a_view(request):
    return redirect('another_view')

def another_view(request):
    return redirect('a_view')

この例は原理を示していますが、非常に単純です。 現実の世界で遭遇するリダイレクトループは、おそらく見つけにくいでしょう。 より複雑な例を見てみましょう:

def featured_products_view(request):
    featured_products = Product.objects.filter(featured=True)
    if len(featured_products == 1):
        return redirect('product_view', kwargs={'product_id': featured_products[0].id})
    return render(request, 'featured_products.html', {'product': featured_products})

def product_view(request, product_id):
    try:
        product = Product.objects.get(pk=product_id, in_stock=True)
    except Product.DoesNotExist:
        return redirect('featured_products_view')
    return render(request, 'product_detail.html', {'product': product})

featured_products_view()は、すべての注目製品、つまり、.featuredTrueに設定されたProductインスタンスをフェッチします。 注目の製品が1つしかない場合は、product_view()に直接リダイレクトされます。 それ以外の場合は、featured_productsクエリセットを使用してテンプレートをレンダリングします。

product_viewは前のセクションから見覚えがありますが、2つの小さな違いがあります。

  • ビューは、.in_stockTrueに設定することで示される、在庫のあるProductをフェッチしようとします。

  • 在庫のある製品がない場合、ビューはfeatured_products_view()にリダイレクトされます。

このロジックは、ショップが成功の犠牲者になり、現在お持ちの機能製品が在庫切れになるまで正常に機能します。 .in_stockFalseに設定したが、.featuredFalseに設定し忘れた場合、feature_product_view()への訪問者はリダイレクトループでスタックします。 。

この種のバグを防ぐ防弾の方法はありませんが、適切な出発点は、リダイレクト先のビューがリダイレクト自体を使用しているかどうかを確認することです。

永続的なリダイレクトは永続的です

パーマネントリダイレクトは悪いタトゥーのようなものです。それらは当時は良いアイデアのように思えるかもしれませんが、一度間違いだと気づいたら、それらを取り除くのは非常に困難です。

ブラウザがURLの永続的なリダイレクト応答を受信すると、この応答を無期限にキャッシュします。 将来、古いURLをリクエストするたびに、ブラウザはそのURLをロードせずに、新しいURLを直接ロードします。

ブラウザーに、永続的なリダイレクトを返したURLをロードするように説得するのは非常に難しい場合があります。 Google Chromeは、リダイレクトのキャッシュに関して特に積極的です。

なぜこれが問題になるのでしょうか?

Djangoを使用してWebアプリケーションを構築するとします。 ドメインをmyawesomedjangowebapp.comで登録します。 最初のステップとして、https://myawesomedjangowebapp.com/blog/にブログアプリをインストールして、起動メーリングリストを作成します。

https://myawesomedjangowebapp.com/にあるサイトのホームページはまだ作成中であるため、https://myawesomedjangowebapp.com/blog/にリダイレクトします。 パーマネントリダイレクトはキャッシュされると聞いたため、パーマネントリダイレクトを使用することにしました。キャッシュはGoogleの検索結果のランキングの要因であるため、キャッシングはより高速になります。

結局のところ、あなたは優れた開発者であるだけでなく、才能のある作家でもあります。 ブログが人気になり、ローンチメーリングリストが拡大します。 数か月後、アプリの準備が整います。 現在、光沢のあるホームページがあり、最終的にリダイレクトを削除します。

特別な割引コードを記載したアナウンスメールを、かなり大きなローンチメーリングリストに送信します。 身を乗り出し、サインアップ通知が届くのを待ちます。

恐ろしいことに、メールボックスには、アプリにアクセスしたいが、常にブログにリダイレクトされている混乱した訪問者からのメッセージがいっぱいになります。

何が起きたの? https://myawesomedjangowebapp.com/blog/へのリダイレクトがまだアクティブなときに、ブログの読者がhttps://myawesomedjangowebapp.com/にアクセスしていました。 これは永続的なリダイレクトであるため、ブラウザにキャッシュされていました。

ローンチアナウンスメールのリンクをクリックしても、ブラウザーは新しいホームページを確認することをせず、ブログに直接アクセスしました。 起動の成功を祝う代わりに、chrome://net-internalsをいじってブラウザのキャッシュをリセットする方法をユーザーに指示するのに忙しいのです。

永続的なリダイレクトの永続的な性質は、ローカルマシンでの開発中に噛みつくこともあります。 myawesomedjangowebapp.comにその運命的なパーマネントリダイレクトを実装した瞬間に巻き戻しましょう。

開発サーバーを起動し、http://127.0.0.1:8000/を開きます。 意図したとおり、アプリはブラウザをhttp://127.0.0.1:8000/blog/にリダイレクトします。 作業に満足したら、開発サーバーを停止して昼食に行きます。

あなたは完全な腹を持って戻り、クライアントの仕事に取り組む準備ができています。 クライアントはホームページの簡単な変更を望んでいるので、クライアントのプロジェクトをロードして開発サーバーを起動します。

しかし、ここで何が起こっているのでしょうか? ホームページは壊れており、404を返します! 午後の不振により、クライアントのプロジェクトに存在しないhttp://127.0.0.1:8000/blog/にリダイレクトされていることに気付くまでにしばらく時間がかかります。

ブラウザにとって、URLhttp://127.0.0.1:8000/が完全に異なるアプリケーションを提供するようになったことは問題ではありません。 ブラウザにとって重要なのは、このURLが過去に1回、http://127.0.0.1:8000/blog/への永続的なリダイレクトを返したことです。

この話から得られることは、今後二度と使用するつもりのないURLに対してのみ永続的なリダイレクトを使用することです。 恒久的なリダイレクトの場所がありますが、その結果に注意する必要があります。

本当に恒久的なリダイレクトが必要であると確信している場合でも、最初に一時的なリダイレクトを実装し、すべてが意図したとおりに機能することを100%確信できたら、恒久的な従兄弟に切り替えることをお勧めします。

未検証のリダイレクトはセキュリティを侵害する可能性があります

セキュリティの観点から、リダイレクトは比較的安全な手法です。 攻撃者はリダイレクトを使用してWebサイトをハッキングすることはできません。 結局のところ、リダイレクトは、攻撃者がブラウザのアドレスバーに入力するだけのURLにリダイレクトするだけです。

ただし、リダイレクトURLとして適切に検証せずに、URLパラメーターなどの何らかの種類のユーザー入力を使用すると、フィッシング攻撃のために攻撃者に悪用される可能性があります。 この種のリダイレクトはopen or unvalidated redirectと呼ばれます。

ユーザー入力から読み取られたURLにリダイレクトするための正当なユースケースがあります。 代表的な例は、Djangoのログインビューです。 ログイン後にユーザーがリダイレクトされるページのURLを含むURLパラメーターnextを受け入れます。 ログイン後にユーザーをプロファイルにリダイレクトするには、URLは次のようになります。

https://myawesomedjangowebapp.com/login/?next=/profile/

Djangoはnextパラメータを検証しますが、検証しないと少しの間仮定しましょう。

検証がなければ、攻撃者はURLを作成して、ユーザーを自分の管理下にあるWebサイトにリダイレクトする可能性があります。例

https://myawesomedjangowebapp.com/login/?next=https://myawesomedjangowebapp.co/profile/

Webサイトmyawesomedjangowebapp.coは、エラーメッセージを表示し、ユーザーをだまして資格情報を再度入力させる可能性があります。

オープンリダイレクトを回避する最善の方法は、リダイレクトURLを作成するときにユーザー入力を使用しないことです。

URLがリダイレクトに対して安全であるかどうか確信が持てない場合は、関数django.utils.http.is_safe_url()を使用してURLを検証できます。 docstringはその使用法を非常によく説明しています:

is_safe_url(url, host=None, allowed_hosts=None, require_https=False)

URLが安全なリダイレクトである場合(つまり、Trueを返します。 別のホストを指すものではなく、安全なスキームを使用しています)。 空のURLでは常にFalseを返します。 require_httpsTrueの場合、デフォルトのFalseの「http」と「https」とは対照的に、「https」のみが有効なスキームと見なされます。 (Source

いくつかの例を見てみましょう。

相対URLは安全と見なされます。

>>>

>>> # Import the function first.
>>> from django.utils.http import is_safe_url
>>> is_safe_url('/profile/')
True

別のホストを指すURLは一般に安全とは見なされません。

>>>

>>> is_safe_url('https://myawesomedjangowebapp.com/profile/')
False

別のホストを指すURLは、そのホストがallowed_hostsで提供されている場合、安全であると見なされます。

>>>

>>> is_safe_url('https://myawesomedjangowebapp.com/profile/',
...             allowed_hosts={'myawesomedjangowebapp.com'})
True

引数require_httpsTrueの場合、httpスキームを使用するURLは安全とは見なされません。

>>>

>>> is_safe_url('http://myawesomedjangowebapp.com/profile/',
...             allowed_hosts={'myawesomedjangowebapp.com'},
...             require_https=True)
False

概要

これは、DjangoでのHTTPリダイレクトに関するこのガイドのまとめです。 おめでとうございます:これで、リダイレクトのあらゆる側面について、HTTPプロトコルの低レベルの詳細からDjangoでの高レベルの方法に至るまで触れました。

HTTPリダイレクトが内部でどのように見えるか、さまざまなステータスコードが何であるか、永続リダイレクトと一時リダイレクトがどのように異なるかを学びました。 この知識はDjangoに固有のものではなく、あらゆる言語でのWeb開発に役立ちます。

リダイレクト応答クラスHttpResponseRedirectHttpResponsePermanentRedirectを使用するか、便利な関数django.shortcuts.redirect()を使用して、Djangoでリダイレクトを実行できるようになりました。 いくつかの高度なユースケースのソリューションを見て、一般的な落とし穴を避ける方法を知っています。

HTTPリダイレクトについてさらに質問がある場合は、下にコメントを残してください。