Найкращий спосіб зробити для Django login_require за замовчуванням


103

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

@login_required
def view(...):

Це добре, і це чудово працює , поки ми пам'ятаємо його додавати скрізь ! На жаль, ми іноді забуваємо, і невдача часто не є надзвичайно очевидною. Якщо єдине посилання на представлення даних перебуває на сторінці @login_required, ви, швидше за все, не помітите, що ви можете дійсно дійти до цього виду, не входячи в систему. Але погані хлопці можуть помітити, що є проблемою.

Моя ідея полягала в тому, щоб змінити систему. Замість того, щоб вводити @login_required всюди, натомість я маю щось подібне:

@public
def public_view(...):

Просто для публічних речей. Я спробував реалізувати це за допомогою деякого проміжного програмного забезпечення, і я не міг здатися, що це працює. Я думаю, все, що я намагався, погано взаємоділо з іншими проміжними програмами, які ми використовуємо. Далі я спробував щось написати, щоб відмітити шаблони URL-адрес, щоб перевірити, чи все, що не було @public, було позначено @login_required - принаймні тоді ми отримаємо швидку помилку, якщо щось забули. Але тоді я не зміг зрозуміти, як сказати, чи було застосовано @login_required до подання ...

Отже, який правильний спосіб зробити це? Дякую за допомогу!


2
Відмінне запитання. Я був у абсолютно такому ж становищі. У нас є середнє програмне забезпечення для створення всього сайту login_required, і у нас є домашні сорти ACL для показу різних поглядів / фрагментів шаблону для різних людей / ролей, але це відрізняється від будь-якого з цих.
Пітер Роуелл

Відповіді:


99

Посереднє програмне забезпечення може бути найкращим для вас. Раніше я використовував цей фрагмент коду, модифікований з фрагмента, знайденого в іншому місці:

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декораторами - це те, що якщо схема автентифікації зміниться, у вас є одне місце, щоб зробити глобальні зміни.


Дякую, це виглядає чудово! Мені не прийшло в голову фактично використовувати login_required () у своєму середньому програмному забезпеченні. Я думаю, що це допоможе вирішити проблему, з якою я гарно грав із нашим стеком програмного забезпечення.
samtregar

До! Це майже точно той шаблон, який ми використовували для групи сторінок, які мали бути HTTPS, а все інше не повинно бути HTTPS. Це було 2,5 роки тому, і я зовсім забув про це. Дякую, Даніеле!
Пітер Роуелл

4
Куди слід розмістити посередницький клас RequireLoginMiddleware? views.py, models.py?
Ясін

1
@richard декоратори працюють у час компіляції, і в цьому випадку все, що я робив, було: function.public = True. Тоді, коли запускається середнє програмне забезпечення, він може шукати прапор .public у функції, щоб вирішити, чи дозволяти доступ чи ні. Якщо це не має сенсу, я можу надіслати вам повний код.
самтрегар

1
Я думаю, що найкращим підходом є створення @publicдекоратора, який встановлює _publicатрибут на перегляд, а середнє програмне забезпечення потім пропускає ці погляди. Дизайнер csrf_exempt Django працює так само
Іван Вірабян

31

Існує альтернатива розміщенню декоратора для кожної функції перегляду. Ви також можете помістити login_required()декоратор у urls.pyфайл. Хоча це все ще ручне завдання, принаймні у вас це все в одному місці, що полегшує аудит.

наприклад,

    з my_views імпортує home_view

    urlpatterns = шаблони ('',
        # "Головна":
        (r '^ $', login_required (home_view), dict (template_name = 'my_site / home.html', items_per_page = 20)),
    )

Зауважте, що функції перегляду називаються та імпортуються безпосередньо, а не як рядки.

Також зауважте, що це працює з будь-яким об'єктом перегляду, який можна викликати, включаючи класи.


3

У 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
дякую за цю нову відповідь. але поясніть, будь ласка, трохи про це, я не зміг би його отримати, навіть якщо я прочитав офіційний документ. заздалегідь дякую за допомогу
Тянь Лун

@ TianLoon, будь ласка, подивіться мою оновлену відповідь, це може допомогти.
анджанді

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

Запустіть це і вивчіть вихід для defs без відповідних декораторів.


2

Ось проміжне програмне забезпечення для django 1.10+

Посереднє програмне забезпечення має бути написане новим способом у django 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. Додати в налаштування.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Джерела:

  1. Ця відповідь Даніеля Нааба

  2. Підручник Django Middleware від Макса Гудріджа

  3. Документи Django Middleware


Зауважте, що хоч нічого і не відбувається __call__, process_viewгак все ще використовується [редагувано]
Simon Kohlmeyer

1

Натхненний відповіддю Бер, я написав невеликий фрагмент, який замінює 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

Ви не можете реально виграти це. Ви просто повинні зробити заяву про вимоги до авторизації. Куди б ви не помістили цю декларацію, крім правильної функції перегляду?

Подумайте про заміну функції перегляду на об'єкти, що дзвоняться.

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()  

Це не зберігає рядки коду. І це не допомагає проблемі "ми забули". Все, що ви можете зробити, це вивчити код, щоб переконатися, що функції перегляду є об'єктами. Правильного класу.

Але навіть тоді ви ніколи не дізнаєтесь, що кожна функція перегляду правильна без набору одиничних тестів.


5
Я не можу перемогти? Але я мушу перемагати! Втратити - це не варіант! Але якщо серйозно, то я не намагаюся уникати декларацій про свої авторські вимоги. Я просто хочу змінити те, що потрібно задекларувати. Замість того, щоб заявляти про всі приватні погляди і нічого не говорити про публічні, я хочу оголосити всі загальнодоступні погляди і мати за замовчуванням приватні.
samtregar

Крім того, ідеальна ідея для занять переглядами ... Але я думаю, що переписування сотень переглядів у моєму додатку в цей момент, мабуть, не є початковим.
samtregar

@samtregar: Ви повинні перемогти? У мене повинен бути новий Bentley. Серйозно. Ви можете поздоровитись def. Ви можете тривіально написати дуже короткий сценарій, щоб просканувати всі defв усіх модулях перегляду та встановити, чи забутий @login_required.
С.Лотт

8
@ S.Lott Це найменший спосіб зробити це, але так, я думаю, це спрацювало б. За винятком того, як ви знаєте, які показники є переглядами? Просто перегляд функцій у Views.py не працюватиме, спільним функціям помічника там не потрібен @login_required.
samtregar

Так, це кульгаво. Майже самий хромий, про який я міг придумати. Ви не знаєте, які показники є переглядачами, за винятком вивчення urls.py.
S.Lott


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.