Руководство по редиректам в Django

Оглавление

Когда вы создаете веб-приложение на Python с помощью фреймворка Django, в какой-то момент вам придется перенаправлять пользователя с одного URL на другой.

В этом руководстве вы узнаете все, что нужно знать о HTTP-перенаправлениях и о том, как работать с ними в Django. В конце этого руководства вы будете:

  • Уметь перенаправлять пользователя с одного URL на другой URL
  • Знать разницу между временными и постоянными перенаправлениями
  • Избегайте распространенных ловушек при работе с редиректами

В этом уроке предполагается, что вы знакомы с основными строительными блоками приложения Django, такими как views и URL patterns.

Django Redirects: Суперпростой пример

В Django вы перенаправляете пользователя на другой URL, возвращая экземпляр HttpResponseRedirect или HttpResponsePermanentRedirect из вашего представления. Самый простой способ сделать это - использовать функцию redirect() из модуля django.shortcuts. Вот пример:

# views.py
from django.shortcuts import redirect

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

Просто вызовите redirect() с URL в вашем представлении. Он вернет класс HttpResponseRedirect, который вы затем вернете из вашего представления.

Представление, возвращающее редирект, должно быть добавлено к вашему urls.py, как и любое другое представление:

# urls.py
from django.urls import path

from .views import redirect_view

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

Полагая, что это главный urls.py вашего проекта Django, URL /redirect/ теперь перенаправляет на /redirect-success/.

Чтобы избежать жесткого кодирования URL, вы можете вызвать redirect() с именем представления или шаблона URL или модели, чтобы избежать жесткого кодирования URL перенаправления. Вы также можете создать постоянное перенаправление, передав аргумент с ключевым словом permanent=True.

Эта статья могла бы закончиться на этом, но тогда ее вряд ли можно было бы назвать "Окончательным руководством по редиректам Django". В ближайшее время мы рассмотрим функцию redirect(), а также разберемся в тонкостях кодов состояния HTTP и различных классов HttpRedirectResponse, но давайте сделаем шаг назад и начнем с фундаментального вопроса.

Зачем перенаправлять

Вы можете задаться вопросом, зачем вообще нужно перенаправлять пользователя на другой URL. Чтобы понять, в каких случаях перенаправление имеет смысл, посмотрите, как сам Django включает перенаправление в функции, которые фреймворк предоставляет по умолчанию:

  • Когда вы не вошли в систему и запрашиваете URL, требующий аутентификации, например, админку Django, Django перенаправляет вас на страницу входа.
  • При успешном входе Django перенаправляет вас на URL, который вы запрашивали первоначально.
  • Когда вы меняете пароль с помощью админки Django, вы перенаправляетесь на страницу, которая показывает, что изменение прошло успешно.
  • Когда вы создаете объект в админке Django, Django перенаправляет вас на список объектов.

Как выглядела бы альтернативная реализация без редиректов? Если для просмотра страницы пользователю необходимо войти в систему, можно просто отобразить страницу с надписью "Нажмите здесь, чтобы войти в систему". Это сработает, но будет неудобно для пользователя.

Сократители URL, такие как http://bit.ly, - еще один пример того, как могут пригодиться перенаправления: вы вводите короткий URL в адресную строку браузера, а затем перенаправляетесь на страницу с длинным, громоздким URL.

Примечание: Если вы хотите создать собственный укорачиватель URL, то ознакомьтесь с Build a URL Shortener With FastAPI and Python.

В других случаях редиректы - это не просто вопрос удобства. Перенаправления - это важный инструмент, который помогает пользователю ориентироваться в веб-приложении. После выполнения какой-либо операции с побочными эффектами, например создания или удаления объекта, лучше всего перенаправить на другой URL, чтобы случайно не выполнить операцию дважды.

Одним из примеров использования редиректов является обработка форм, когда пользователь перенаправляется на другой 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() для рендеринга шаблона contact_form.html.

Если пользователь теперь нажмет кнопку reload, будет перезагружен только URL /success/. Без редиректа перезагрузка страницы привела бы к повторной отправке формы и отправке еще одного сообщения.

За кулисами: как работает HTTP-перенаправление

Теперь вы знаете, почему редиректы имеют смысл, но как они работают? Давайте вкратце расскажем, что происходит, когда вы вводите URL в адресную строку веб-браузера.

Краткое руководство по HTTP

Допустим, вы создали приложение Django с представлением "Hello World", которое обрабатывает путь /hello/. Вы запускаете свое приложение на сервере разработки Django, поэтому полный URL будет http://127.0.0.1:8000/hello/.

Когда вы вводите этот URL в браузер, он подключается к порту 8000 на сервере с IP-адресом 127.0.0.1 и отправляет HTTP GET запрос на путь /hello/. Сервер отвечает 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 Redirects Status Codes

Как выглядит ответ на перенаправление? Предположим, что путь /redirect/ обрабатывается путем redirect_view(), показанным ранее. Если вы обратитесь к http://127.0.0.1:8000/redirect/ с помощью curl, ваша консоль будет выглядеть следующим образом:

$ 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

Эти два ответа могут показаться похожими, но есть несколько ключевых различий. Перенаправление:

  • Возвращает другой код состояния (302 против 200)
  • Содержит заголовок Location с относительным URL
  • Заканчивается пустой строкой, так как тело ответа перенаправления пусто

Основным отличительным признаком является код статуса. В спецификации стандарта HTTP говорится следующее:

Код состояния 302 (Found) указывает на то, что целевой ресурс временно находится под другим URI. Поскольку перенаправление может иногда изменяться, клиент должен продолжать использовать эффективный URI запроса для будущих запросов. Сервер ДОЛЖЕН генерировать поле заголовка Location в ответе, содержащее ссылку на URI для другого URI. Агент пользователя МОЖЕТ использовать значение поля Location для автоматического перенаправления. (Source)

Другими словами, всякий раз, когда сервер отправляет код состояния 302, он говорит клиенту: "Эй, в данный момент то, что вы ищете, можно найти в этом другом месте"

Ключевая фраза в спецификации - "МОЖЕТ использовать значение поля Location для автоматического перенаправления". Это означает, что вы не можете заставить клиента загрузить другой URL. Клиент может решить дождаться подтверждения пользователя или вообще не загружать URL.

Теперь вы знаете, что редирект - это просто HTTP-ответ с кодом состояния 3xx и заголовком Location. Ключевым моментом здесь является то, что HTTP-переадресация - это как любой старый HTTP-ответ, но с пустым телом, кодом состояния 3xx и заголовком Location.

Вот и все. Мы еще вернемся к Django, но сначала давайте рассмотрим два типа редиректов в диапазоне кодов статуса 3xx и поймем, почему они имеют значение для веб-разработки.

Временные и постоянные перенаправления

В стандарте HTTP определено несколько кодов состояния перенаправления, все они находятся в диапазоне 3xx. Два наиболее распространенных кода состояния - 301 Permanent Redirect и 302 Found.

Код состояния 302 Found указывает на временное перенаправление. Временное перенаправление говорит: "В данный момент то, что вы ищете, можно найти по другому адресу". Подумайте об этом, как о вывеске магазина, которая гласит: "Наш магазин сейчас закрыт на ремонт. Пожалуйста, посетите наш другой магазин за углом". Поскольку это временное явление, в следующий раз, когда вы пойдете за покупками, проверьте первоначальный адрес.

Примечание: В HTTP 1.0 сообщение для кода состояния 302 было Temporary Redirect. В HTTP 1.1 это сообщение было изменено на Found.

Как следует из названия, постоянные редиректы должны быть постоянными. Постоянное перенаправление говорит браузеру: "То, что вы ищете, больше не находится по этому адресу. Теперь он находится по этому новому адресу, и больше никогда не будет находиться по старому адресу"

Постоянный редирект - это как вывеска магазина, на которой написано: "Мы переехали. Наш новый магазин находится прямо за углом". Это изменение является постоянным, поэтому в следующий раз, когда вы захотите посетить магазин, вы отправитесь прямо по новому адресу.

Примечание: Постоянные редиректы могут иметь непредвиденные последствия. Дочитайте это руководство до конца, прежде чем использовать постоянный редирект, или сразу переходите к разделу "Постоянные редиректы - это навсегда".

Браузеры ведут себя аналогично при работе с перенаправлениями: когда URL возвращает ответ с постоянным перенаправлением, этот ответ кэшируется. В следующий раз, когда браузер встречает старый URL, он запоминает перенаправление и напрямую запрашивает новый адрес.

Кэширование перенаправления избавляет от лишних запросов и делает работу пользователей более качественной и быстрой.

Кроме того, различие между временными и постоянными редиректами имеет значение для поисковой оптимизации.

Перенаправления в Django

Теперь вы знаете, что редирект - это просто HTTP-ответ с кодом состояния 3xx и заголовком Location.

Вы можете создать такой ответ самостоятельно из обычного HttpResponse объекта:

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

Это решение технически правильное, но оно предполагает довольно большой объем набора текста.

Класс HTTPResponseRedirect

Вы можете избавить себя от необходимости набирать текст с помощью класса HttpResponseRedirect, подкласса HttpResponse. Просто создайте класс, указав в качестве первого аргумента URL, на который вы хотите перенаправить, и класс установит правильный статус и заголовок Location:

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

Вы можете поиграть с классом HttpResponseRedirect в оболочке Python, чтобы увидеть, что получается:

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

Также существует класс для постоянных редиректов, который метко назван HttpResponsePermanentRedirect. Он работает так же, как и HttpResponseRedirect, с той лишь разницей, что имеет код состояния 301 (Moved Permanently).

Примечание: В приведенных выше примерах URL-адреса перенаправления жестко закодированы. Жесткое кодирование URL - плохая практика: если URL когда-нибудь изменится, вам придется искать его во всем коде и менять все вхождения. Давайте это исправим!

Вы можете использовать django.urls.reverse() для построения URL, но есть более удобный способ, который вы увидите в следующем разделе.

Функция redirect()

Чтобы облегчить вам жизнь, Django предоставляет универсальную функцию быстрого доступа, с которой вы уже познакомились во введении: django.shortcuts.redirect().

Вы можете вызвать эту функцию с помощью:

  • Экземпляр модели или любой другой объект с get_absolute_url() методом
  • URL или имя представления и позиционные и/или ключевые аргументы URL

Он выполнит соответствующие действия, чтобы превратить аргументы в URL и вернуть HTTPResponseRedirect. Если вы передадите permanent=True, он вернет экземпляр HttpResponsePermanentRedirect, что приведет к постоянному перенаправлению.

Вот три примера, иллюстрирующие различные варианты использования:

  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_id>/', '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/<term>/', SearchRedirectView.as_view())
]

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

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

Шаблон URL определяет аргумент term, который используется в SearchRedirectView для построения URL перенаправления. Путь /search/kittens/ в вашем приложении будет перенаправлять вас на https://google.com/?q=kittens.

Вместо того, чтобы создавать подкласс RedirectView для перезаписи атрибута url, вы можете передать аргумент ключевого слова url в as_view() в ваших urlpatterns:

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

urlpatterns = [
    path('/search/<term>/',
         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)

Это представление на основе класса перенаправляет на URL, выбранный случайным образом из .animal_urls.

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, переданные представлению, для расширения любых именованных спецификаторов формата.

    Если .url не задан, проверяется, задан ли .pattern_name. Если установлен, то он использует его для разворачивания URL с любыми позиционными и ключевыми аргументами, которые он получил.

    Вы можете изменить это поведение любым удобным для вас способом, переопределив

Примечание: Представления на основе классов - мощная концепция, но разобраться в ней бывает непросто. В отличие от обычных представлений, основанных на функциях, где относительно легко проследить за потоком кода, представления, основанные на классах, состоят из сложной иерархии миксинов и базовых классов.

Отличным инструментом, позволяющим разобраться в классе представлений, основанном на классах, является сайт 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(), чтобы получить отображение URL на product_view().

  2. Далее необходимо построить строку запроса. Это часть после вопросительного знака. Для этого рекомендуется использовать urllib.urlparse.urlencode(), так как он позаботится о правильном кодировании любых специальных символов.

  3. Теперь нужно соединить base_url и query_string вопросительным знаком. Для этого отлично подходит строка формата.

  4. Наконец, вы передаете url в django.shortcuts.redirect() или в класс перенаправления ответа.

  5. В product_view(), вашей цели перенаправления, параметр будет доступен в словаре request.GET. Параметр может отсутствовать, поэтому вам следует использовать requests.GET.get('category') вместо requests.GET['category']. Первый возвращает None, если параметр не существует, а второй вызовет исключение.

Примечание: Обязательно проверяйте любые данные, которые вы читаете из строк запроса. Может показаться, что эти данные находятся под вашим контролем, поскольку вы создали URL-адрес перенаправления.

В действительности редирект может быть подстроен пользователем, и ему нельзя доверять, как и любому другому пользовательскому вводу. Без надлежащей проверки злоумышленник может получить несанкционированный доступ.

Специальные коды перенаправления

Django предоставляет классы HTTP-ответов для кодов состояния 301 и 302. Они должны покрывать большинство случаев использования, но если вам когда-нибудь понадобится возвращать коды состояния 303, 307 или 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

Примечание: На самом деле существует третий класс с кодом состояния в диапазоне 3xx: HttpResponseNotModified, с кодом состояния 304. Он указывает, что URL содержимого не изменился и клиент может использовать кэшированную версию.

Можно было бы утверждать, что ответ 304 Not Modified перенаправляет на кэшированную версию URL, но это несколько натянуто. Следовательно, он больше не указан в разделе "Перенаправление 3xx" стандарта HTTP.

Питфоллы

Перенаправления, которые просто не перенаправляют

Простота 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})

Теперь вы хотите добавить второе представление для отображения отзывов покупателей о товаре. Он также должен перенаправлять на главную страницу для несуществующих продуктов, поэтому в качестве первого шага вы извлекаете эту функциональность из 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})

К сожалению, после рефакторинга редирект больше не работает.

Результат redirect() возвращается из get_product_or_redirect(), но product_view() не возвращает его. Вместо этого он передается в шаблон.

В зависимости от того, как вы используете переменную product в шаблоне product_detail.html, это может не привести к сообщению об ошибке и просто отобразить пустые значения.

Перенаправления, которые не перестают перенаправлять

При работе с перенаправлениями вы можете случайно создать цикл перенаправления, когда URL A возвращает перенаправление, указывающее на URL B, который возвращает перенаправление на URL A, и так далее. Большинство HTTP-клиентов обнаруживают такую петлю перенаправления и после нескольких запросов выдают сообщение об ошибке.

К сожалению, обнаружить такую ошибку бывает непросто, поскольку на стороне сервера все выглядит нормально. Если ваши пользователи не жалуются на эту проблему, единственным признаком того, что что-то не так, является наличие нескольких запросов от одного клиента, которые быстро приводят к ответу в виде редиректа, но не дают ответа со статусом 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() получает все представленные товары, другими словами Product экземпляров с .featured, установленным на True. Если существует только один товар, он перенаправляет непосредственно на product_view(). В противном случае создается шаблон с набором запросов featured_products.

Выглядит product_view знакомо по предыдущему разделу, но имеет два небольших отличия:

  • Представление пытается получить Product, который находится на складе, на что указывает наличие .in_stock, установленного на True.
  • Представление перенаправляет на featured_products_view(), если товара нет на складе.

Эта логика прекрасно работает до тех пор, пока ваш магазин не становится жертвой собственного успеха, и единственный товар, который вы сейчас предлагаете, не исчезает из продажи. Если вы установите .in_stock на False, но забудете установить .featured на False, то любой посетитель вашего feature_product_view() застрянет в цикле перенаправления.

Не существует безотказного способа предотвращения такого рода ошибок, но хорошей отправной точкой будет проверка того, использует ли представление, на которое вы перенаправляете, редиректы самостоятельно.

Постоянные перенаправления являются постоянными

Постоянные редиректы могут быть похожи на плохие татуировки: в то время они могут показаться хорошей идеей, но когда вы поймете, что это была ошибка, избавиться от них будет довольно сложно.

Когда браузер получает постоянный ответ перенаправления для URL-адреса, он кэширует этот ответ на неопределенное время. Если в будущем вы запросите старый URL, браузер не будет загружать его и сразу загрузит новый.

Убедить браузер загрузить URL, который однажды вернул постоянное перенаправление, бывает довольно сложно. Google Chrome особенно агрессивен, когда дело доходит до кэширования перенаправлений.

Почему это может быть проблемой?

Представьте, что вы хотите создать веб-приложение с помощью Django. Вы регистрируете свой домен по адресу myawesomedjangowebapp.com. В качестве первого шага вы устанавливаете приложение для блога по адресу https://myawesomedjangowebapp.com/blog/, чтобы создать список рассылки.

Главная страница вашего сайта по адресу https://myawesomedjangowebapp.com/ все еще находится в стадии разработки, поэтому вы перенаправляете на https://myawesomedjangowebapp.com/blog/. Вы решили использовать постоянный редирект, потому что слышали, что постоянные редиректы кэшируются, а кэширование ускоряет работу, а быстрее - значит лучше, потому что скорость - это фактор ранжирования в результатах поиска Google.

Как оказалось, вы не только отличный разработчик, но и талантливый писатель. Ваш блог становится популярным, а список рассылки для запуска растет. Через пару месяцев ваше приложение готово. Теперь у него блестящая домашняя страница, и вы наконец-то убрали редирект.

Вы рассылаете анонсирующее письмо со специальным кодом скидки по своему большому списку рассылки, посвященному запуску. Вы откидываетесь на спинку кресла и ждете, когда начнут поступать уведомления о подписке.

К вашему ужасу, ваш почтовый ящик заполняется сообщениями от растерянных посетителей, которые хотят посетить ваше приложение, но постоянно перенаправляются в ваш блог.

Что случилось? Читатели вашего блога посетили https://myawesomedjangowebapp.com/, когда редирект на https://myawesomedjangowebapp.com/blog/ был еще активен. Поскольку это был постоянный редирект, он был кэширован в их браузерах.

Когда они нажимали на ссылку в вашем письме с анонсом запуска, их браузеры не удосуживались проверить вашу новую домашнюю страницу и сразу переходили на ваш блог. Вместо того чтобы праздновать успешный запуск, вы заняты тем, что инструктируете своих пользователей, как возиться с 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/, которого не существует в проекте клиента.

Для браузера не имеет значения, что URL http://127.0.0.1:8000/ теперь служит совершенно другому приложению. Для браузера важно лишь то, что этот URL когда-то в прошлом возвращал постоянное перенаправление на http://127.0.0.1:8000/blog/.

Вывод из этой истории таков: используйте постоянные редиректы только на тех URL, которые вы не собираетесь больше использовать. Постоянным редиректам есть место, но вы должны знать об их последствиях.

Даже если вы уверены, что вам действительно нужен постоянный редирект, лучше сначала внедрить временный редирект и переключиться на его постоянный собрат только после того, как вы на 100% убедитесь, что все работает так, как нужно.

Недействительные перенаправления могут нарушить безопасность

С точки зрения безопасности, редиректы - относительно безопасная техника. Злоумышленник не сможет взломать сайт с помощью редиректа. В конце концов, редирект просто перенаправляет на URL, который злоумышленник может просто набрать в адресной строке своего браузера.

Однако если вы используете какой-либо пользовательский ввод, например параметр URL, без надлежащей проверки в качестве URL перенаправления, это может быть использовано злоумышленником для фишинговой атаки. Такой вид перенаправления называется открытым или невалидированным перенаправлением.

Существуют законные случаи использования перенаправления на URL, который считывается с пользовательского ввода. Ярким примером является представление входа в систему Django. Оно принимает параметр URL next, который содержит URL страницы, на которую пользователь будет перенаправлен после входа. Чтобы перенаправить пользователя в его профиль после входа, URL может выглядеть следующим образом:

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

Django действительно проверяет параметр next, но давайте на секунду предположим, что это не так.

Без проверки злоумышленник может создать URL, перенаправляющий пользователя на подконтрольный ему сайт, например:

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

Затем веб-сайт myawesomedjangowebapp.co может вывести сообщение об ошибке и обманом заставить пользователя снова ввести свои учетные данные.

Лучший способ избежать открытых перенаправлений - не использовать пользовательский ввод при построении URL-адреса перенаправления.

Если вы не можете быть уверены, что URL безопасен для перенаправления, вы можете использовать функцию django.utils.http.is_safe_url() для его проверки. Докстринг хорошо объясняет ее использование:

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

Возвращайте True, если урл является безопасным перенаправлением (т.е. не указывает на другой хост и использует безопасную схему). Всегда возвращайте False при пустом url. Если require_https имеет значение True, только 'https' будет считаться допустимой схемой, в отличие от 'http' и 'https', используемых по умолчанию, False. (Источник)

Давайте рассмотрим несколько примеров.

Относительный 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_https равен True, URL, использующий схему http, не считается безопасным:

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

Резюме

На этом мы завершаем это руководство по HTTP-перенаправлениям в Django. Поздравляем: теперь вы затронули все аспекты редиректов, начиная с низкоуровневых деталей протокола HTTP и заканчивая высокоуровневыми способами работы с ними в Django.

Вы узнали, как выглядит HTTP-перенаправление, что такое различные коды состояния и чем отличаются постоянные и временные перенаправления. Эти знания не являются специфическими для Django и ценны для веб-разработки на любом языке.

Теперь вы можете выполнять перенаправление в Django, используя классы ответов перенаправления HttpResponseRedirect и HttpResponsePermanentRedirect, или удобную функцию django.shortcuts.redirect(). Вы увидели решения для нескольких продвинутых случаев использования и знаете, как избежать распространенных подводных камней.

Если у вас есть еще какие-либо вопросы о HTTP-перенаправлениях, оставьте комментарий ниже, а пока - счастливого перенаправления!

Ссылки

Вернуться на верх