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


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 на функции, чтобы решить, разрешить ли доступ или нет. Если это не имеет смысла, я могу отправить вам полный код.
samtregar 03

1
Я думаю, что лучший подход - создать @publicдекоратор, который устанавливает _publicатрибут для представления, а промежуточное программное обеспечение затем пропускает эти представления. Декоратор csrf_exempt в Django работает точно так же
Иван Вирабян

31

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

например,

    from my_views import home_view

    urlpatterns = patterns ('',
        # "Домой":
        (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
спасибо за этот новый ответ. но, пожалуйста, объясните немного об этом, я не мог понять, даже если я прочитал официальный документ. заранее спасибо за помощь
Tian Loon

@TianLoon, пожалуйста, посмотрите мой обновленный ответ, это может помочь.
Andyandy

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. Добавьте в свой settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Источники:

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

  2. Руководство по Django Middleware от Макса Гудриджа

  3. Документы по промежуточному программному обеспечению Django


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

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: Вы должны побеждать? Мне нужен новый Бентли. Шутки в сторону. Вы можете использовать grep для def's. Вы можете легко написать очень короткий скрипт для сканирования всех defмодулей просмотра и определения, не был ли забыт @login_required.
S.Lott

8
@ S.Lott Это самый неудачный способ сделать это, но да, я думаю, это сработает. Кроме того, как узнать, какие определения являются представлениями? Просто посмотреть на функции в views.py не получится, вспомогательные общие функции там не нуждаются в @login_required.
samtregar

Да, это отстой. Почти самый отстойный, о котором я мог думать. Вы не узнаете, какие определения являются представлениями, кроме как изучив файл urls.py.
S.Lott


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.