Лучший способ сделать Django login_required по умолчанию

87

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

@login_required
def view(...):

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

Моя идея состояла в том, чтобы повернуть вспять систему. Вместо того, чтобы набирать @login_required везде, вместо этого у меня было бы что-то вроде:

@public
def public_view(...):

Просто для публики. Я попытался реализовать это с помощью некоторого промежуточного ПО, и я не мог заставить его работать. Я думаю, все, что я пробовал, плохо взаимодействует с другим промежуточным программным обеспечением, которое мы используем. Затем я попытался написать что-то, чтобы пересечь шаблоны URL, чтобы проверить, что все, что не @public было отмечено @login_required, - по крайней мере, мы бы быстро поняли ошибку, если мы что-то забыли. Но тогда я не мог понять, как сказать, было ли применено @login_required к представлению...

Итак, каков правильный способ сделать это? Спасибо за помощь!

  • 1
    Отличный вопрос. Я был в точно такой же позиции. У нас есть промежуточное ПО для создания всего сайта login_required, и у нас есть собственный список ACL для отображения разных видов / фрагментов шаблонов для разных людей / ролей, но это отличается от любого из них.
Теги:

8 ответов

93
Лучший ответ

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

import re

from django.conf import settings
from django.contrib.auth.decorators import login_required


class RequireLoginMiddleware(object):
    """
    Middleware component that wraps the login_required decorator around
    matching URL patterns. To use, add the class to MIDDLEWARE_CLASSES and
    define LOGIN_REQUIRED_URLS and LOGIN_REQUIRED_URLS_EXCEPTIONS in your
    settings.py. For example:
    ------
    LOGIN_REQUIRED_URLS = (
        r'/topsecret/(.*)$',
    )
    LOGIN_REQUIRED_URLS_EXCEPTIONS = (
        r'/topsecret/login(.*)$',
        r'/topsecret/logout(.*)$',
    )
    ------
    LOGIN_REQUIRED_URLS is where you define URL patterns; each pattern must
    be a valid regex.

    LOGIN_REQUIRED_URLS_EXCEPTIONS is, conversely, where you explicitly
    define any exceptions (like login and logout URLs).
    """
    def __init__(self):
        self.required = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS)
        self.exceptions = tuple(re.compile(url) for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS)

    def process_view(self, request, view_func, view_args, view_kwargs):
        # No need to process URLs if user already logged in
        if request.user.is_authenticated():
            return None

        # An exception match should immediately return None
        for url in self.exceptions:
            if url.match(request.path):
                return None

        # Requests matching a restricted URL pattern are returned
        # wrapped with the login_required decorator
        for url in self.required:
            if url.match(request.path):
                return login_required(view_func)(request, *view_args, **view_kwargs)

        # Explicitly return None for all non-matching requests
        return None

Затем в settings.py укажите базовые URL-адреса, которые вы хотите защитить:

LOGIN_REQUIRED_URLS = (
    r'/private_stuff/(.*)$',
    r'/login_required/(.*)$',
)

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

То, что мне нравится в этом подходе - помимо устранения необходимости засорения кода с помощью декораторов @login_required - это то, что если схема аутентификации изменится, у вас есть одно место для внесения глобальных изменений.

  • 0
    Спасибо, это выглядит великолепно! Мне даже не пришло в голову использовать login_required () в моем промежуточном программном обеспечении. Я думаю, что это поможет обойти проблему, с которой я хорошо играл в нашем стеке промежуточного программного обеспечения.
  • 0
    Doh! Это почти точно шаблон, который мы использовали для группы страниц, которые должны были быть HTTPS, а все остальное не должно быть HTTPS. Это было 2,5 года назад, и я полностью забыл об этом. Спасибо, Даниэль!
Показать ещё 9 комментариев
27

Существует альтернатива размещению декоратора для каждой функции представления. Вы также можете поместить декоратор login_required() в файл urls.py. Хотя это все еще ручная задача, по крайней мере, у вас есть все в одном месте, что упрощает аудит.

например.

    from my_views import home_view

    urlpatterns = patterns('',
        # "Home":
        (r'^$', login_required(home_view), dict(template_name='my_site/home.html', items_per_page=20)),
    )

Обратите внимание, что функции просмотра называются и импортируются напрямую, а не как строки.

Также обратите внимание, что это работает с любым вызываемым объектом вида, включая классы.

2

Трудно изменить встроенные предположения в Django, не перерабатывая способ передачи URL для просмотра функций.

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

import os
import re

def view_modules( root ):
    for path, dirs, files in os.walk( root ):
        for d in dirs[:]:
            if d.startswith("."):
                dirs.remove(d)
        for f in files:
            name, ext = os.path.splitext(f)
            if ext == ".py":
                if name == "views":
                    yield os.path.join( path, f )

def def_lines( root ):
    def_pat= re.compile( "\n(\S.*)\n+(^def\s+.*:$)", re.MULTILINE )
    for v in view_modules( root ):
        with open(v,"r") as source:
            text= source.read()
            for p in def_pat.findall( text ):
                yield p

def report( root ):
    for decorator, definition in def_lines( root ):
        print decorator, definition

Запустите это и просмотрите вывод для def без соответствующих декораторов.

1

Вот промежуточное решение для django 1. 10+

Средние слои должны быть написаны по-новому в джанго 1. 10+.

Код

import re

from django.conf import settings
from django.contrib.auth.decorators import login_required


class RequireLoginMiddleware(object):

    def __init__(self, get_response):
         # One-time configuration and initialization.
        self.get_response = get_response

        self.required = tuple(re.compile(url)
                              for url in settings.LOGIN_REQUIRED_URLS)
        self.exceptions = tuple(re.compile(url)
                                for url in settings.LOGIN_REQUIRED_URLS_EXCEPTIONS)

    def __call__(self, request):

        response = self.get_response(request)
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):

        # No need to process URLs if user already logged in
        if request.user.is_authenticated:
            return None

        # An exception match should immediately return None
        for url in self.exceptions:
            if url.match(request.path):
                return None

        # Requests matching a restricted URL pattern are returned
        # wrapped with the login_required decorator
        for url in self.required:
            if url.match(request.path):
                return login_required(view_func)(request, *view_args, **view_kwargs)

        # Explicitly return None for all non-matching requests
        return None

Монтаж

  1. Скопируйте код в папку проекта и сохраните его как middleware.py
  2. Добавить в MIDDLEWARE

    MIDDLEWARE = [... '.middleware.RequireLoginMiddleware', # Требовать логин]

  3. Добавьте в свой settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Источники:

  1. Этот ответ Даниила Нааба

  2. Учебник Django Middleware от Max Goodridge

  3. Django Middleware Docs

  • 0
    Обратите внимание, что хотя в __call__ ничего не происходит, __call__ process_view все еще используется [отредактировано]
1

Вдохновленный ответом Ber, я написал небольшой фрагмент, который заменяет функцию patterns, обертывая все обратные вызовы URL с помощью декоратора login_required. Это работает в Django 1.6.

def login_required_patterns(*args, **kw):
    for pattern in patterns(*args, **kw):
        # This is a property that should return a callable, even if a string view name is given.
        callback = pattern.callback

        # No property setter is provided, so this will have to do.
        pattern._callback = login_required(callback)

        yield pattern

С его помощью это работает (вызов list требуется из-за yield).

urlpatterns = list(login_required_patterns('', url(r'^$', home_view)))
0

В Django 2.1 мы можем украсить все методы в классе с помощью:

from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views.generic import TemplateView

@method_decorator(login_required, name='dispatch')
class ProtectedView(TemplateView):
    template_name = 'secret.html'

ОБНОВЛЕНИЕ: я также нашел следующее, чтобы работать:

from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView

class ProtectedView(LoginRequiredMixin, TemplateView):
    template_name = 'secret.html'

и установите LOGIN_URL = '/accounts/login/' в ваших settings.py

  • 1
    спасибо за этот новый ответ. но, пожалуйста, объясните немного об этом, я не смог бы получить это, даже если я прочитал официальный документ. спасибо за помощь заранее
  • 0
    @TianLoon, пожалуйста, посмотрите мой обновленный ответ, это может помочь.
0

Было бы возможно иметь единственную начальную точку для всех urls в виде include и украшать ее с помощью этих пакетов https://github.com/vorujack/decorate_url.

0

Вы не можете выиграть это. Вы просто должны сделать выражение о требованиях авторизации. Где еще вы могли бы поместить это объявление, кроме права на функцию просмотра?

Рассмотрите возможность замены функций просмотра вызываемыми объектами.

class LoginViewFunction( object ):
    def __call__( self, request, *args, **kw ):
        p1 = self.login( request, *args, **kw )
        if p1 is not None:
            return p1
        return self.view( request, *args, **kw )
    def login( self, request )
        if not request.user.is_authenticated():
            return HttpResponseRedirect('/login/?next=%s' % request.path)
    def view( self, request, *args, **kw ):
        raise NotImplementedError

Затем вы создаете подклассы функций вида LoginViewFunction.

class MyRealView( LoginViewFunction ):
    def view( self, request, *args, **kw ):
        .... the real work ...

my_real_view = MyRealView()  

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

Но даже тогда вы никогда не узнаете, что каждая функция просмотра верна без набора unit test.

  • 0
    Я никогда не думал об этом подходе раньше ... спасибо
  • 5
    Я не могу победить? Но я должен победить! Потеря не вариант! А если серьезно, я не пытаюсь избежать объявления моих требований к аутентификации. Я просто хочу изменить то, что должно быть объявлено. Вместо того, чтобы объявлять все частные взгляды и ничего не говорить о публичных, я хочу объявить все публичные взгляды и сделать их по умолчанию закрытыми.
Показать ещё 5 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню