Najlepszy sposób, aby ustawić login_required w Django jako domyślny


103

Pracuję nad dużą aplikacją Django, do której większość wymaga logowania, aby uzyskać dostęp. Oznacza to, że w całej naszej aplikacji posypaliśmy:

@login_required
def view(...):

W porządku i działa świetnie, o ile pamiętamy o dodawaniu go wszędzie ! Niestety czasami zapominamy, a niepowodzenie często nie jest tak ewidentne. Jeśli jedyny link do widoku znajduje się na stronie @login_required, prawdopodobnie nie zauważysz, że możesz uzyskać dostęp do tego widoku bez logowania. Ale złoczyńcy mogą to zauważyć, co jest problemem.

Mój pomysł polegał na odwróceniu systemu. Zamiast wpisywać @login_required wszędzie, zamiast tego miałbym coś takiego:

@public
def public_view(...):

Tylko do spraw publicznych. Próbowałem zaimplementować to za pomocą oprogramowania pośredniczącego i nie mogłem go uruchomić. Myślę, że wszystko, co próbowałem, źle współdziałało z innym oprogramowaniem pośredniczącym, którego używamy. Następnie próbowałem napisać coś, aby przejść przez wzorce adresów URL, aby sprawdzić, czy wszystko, co nie jest @public, zostało oznaczone jako @login_required - przynajmniej wtedy otrzymywaliśmy szybki błąd, gdybyśmy czegoś zapomnieli. Ale potem nie mogłem dowiedzieć się, jak stwierdzić, czy @login_required został zastosowany do widoku ...

Więc jaki jest właściwy sposób, aby to zrobić? Dzięki za pomoc!


2
Świetne pytanie. Byłem dokładnie w tej samej sytuacji. Mamy oprogramowanie pośredniczące, które sprawia, że logowanie_wymagane jest w całej witrynie , i mamy rodzajem listy ACL stworzonej w domu, służącej do wyświetlania różnych widoków / fragmentów szablonów różnym osobom / rolom, ale różni się to od każdego z nich.
Peter Rowell,

Odpowiedzi:


99

Oprogramowanie pośredniczące może być najlepszym rozwiązaniem. W przeszłości korzystałem z tego fragmentu kodu, zmodyfikowanego na podstawie fragmentu znalezionego w innym miejscu:

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

Następnie w settings.py wypisz podstawowe adresy URL, które chcesz chronić:

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

Tak długo, jak Twoja witryna przestrzega konwencji adresów URL dla stron wymagających uwierzytelniania, ten model będzie działał. Jeśli nie jest to dopasowanie jeden do jednego, możesz zmodyfikować oprogramowanie pośredniczące, aby lepiej pasowało do twoich okoliczności.

To, co podoba mi się w tym podejściu - poza usunięciem konieczności zaśmiecania bazy kodu @login_requireddekoratorami - polega na tym, że jeśli zmieni się schemat uwierzytelniania, masz jedno miejsce, w którym możesz dokonać zmian globalnych.


Dzięki, wygląda świetnie! Nie przyszło mi do głowy, aby faktycznie użyć login_required () w moim oprogramowaniu pośrednim. Myślę, że pomoże to obejść problem, z którym grałem dobrze z naszym stosem oprogramowania pośredniego.
samtregar

No! To jest prawie dokładnie taki sam wzorzec, jakiego użyliśmy dla grupy stron, które musiały być HTTPS, a wszystko inne nie może być HTTPS. To było 2,5 roku temu i zupełnie o tym zapomniałem. Dzięki, Daniel!
Peter Rowell,

4
Klasa oprogramowania pośredniego RequireLoginMiddleware powinna zostać umieszczona gdzie? views.py, models.py?
Yasin

1
Dekoratory @richard działają w czasie kompilacji iw tym przypadku jedyne co zrobiłem to: function.public = True. Następnie, gdy oprogramowanie pośredniczące zostanie uruchomione, może poszukać flagi .public na funkcji, aby zdecydować, czy zezwolić na dostęp, czy nie. Jeśli to nie ma sensu, mogę przesłać pełny kod.
samtregar

1
Myślę, że najlepszym podejściem jest utworzenie @publicdekoratora, który ustawia _publicatrybut na widok, a oprogramowanie pośredniczące pomija te widoki. Dekorator csrf_exempt Django działa w ten sam sposób
Ivan Virabyan

31

Istnieje alternatywa dla umieszczenia dekoratora na każdej funkcji widoku. Możesz również umieścić login_required()dekorator w urls.pypliku. Chociaż jest to nadal zadanie ręczne, przynajmniej masz wszystko w jednym miejscu, co ułatwia audyt.

na przykład,

    z my_views zaimportuj home_view

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

Zwróć uwagę, że funkcje widoku są nazywane i importowane bezpośrednio, a nie jako ciągi.

Zauważ również, że działa to z każdym wywoływalnym obiektem widoku, w tym klasami.


3

W Django 2.1 możemy ozdobić wszystkie metody w klasie za pomocą:

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'

AKTUALIZACJA: Zauważyłem również, że działa:

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

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

i ustawić LOGIN_URL = '/accounts/login/'w swoim settings.py


1
dzięki za tę nową odpowiedź. ale proszę wyjaśnij trochę o tym, nie mogłem tego dostać, nawet jeśli przeczytałem oficjalny dokument. z góry dziękuję za pomoc
Tian Loon

@TianLoon, zobacz moją zaktualizowaną odpowiedź, może to pomóc.
andyandy

2

Trudno jest zmienić wbudowane założenia w Django bez zmiany sposobu, w jaki adresy URL są przekazywane do wyświetlania funkcji.

Zamiast grzebać w wewnętrznych elementach Django, oto audyt, którego możesz użyć. Po prostu sprawdź każdą funkcję widoku.

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

Uruchom to i sprawdź wynik pod kątem defs bez odpowiednich dekoratorów.


2

Oto rozwiązanie pośredniczące dla django 1.10+

Oprogramowanie pośrednie musi być napisane w nowy sposób w django 1.10+ .

Kod

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

Instalacja

  1. Skopiuj kod do folderu projektu i zapisz jako middleware.py
  2. Dodaj do oprogramowania MIDDLEWARE

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Wymagaj logowania]

  3. Dodaj do swojego settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Źródła:

  1. Ta odpowiedź Daniela Naaba

  2. Samouczek Django Middleware autorstwa Max Goodridge

  3. Dokumenty oprogramowania pośredniego Django


Zauważ, że chociaż nic się nie dzieje __call__, process_viewhaczyk jest nadal używany [edytowany]
Simon Kohlmeyer

1

Zainspirowany odpowiedzią Bera, napisałem krótki fragment, który zastępuje patternsfunkcję, opakowując wszystkie wywołania zwrotne adresu URL login_requireddekoratorem. Działa to w 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

Używanie go działa w ten sposób (wywołanie listjest wymagane z powodu yield).

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

0

Naprawdę nie możesz tego wygrać. Po prostu musi złożyć deklarację wymogów autoryzacyjnych. Gdzie indziej umieściłbyś tę deklarację poza funkcją widoku?

Rozważ zastąpienie funkcji widoku obiektami, które można wywołać.

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

Następnie tworzysz podklasy funkcji widoku LoginViewFunction.

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

my_real_view = MyRealView()  

Nie zapisuje żadnych linii kodu. I to nie pomaga w problemie „zapomnieliśmy”. Wszystko, co możesz zrobić, to zbadać kod, aby upewnić się, że funkcje widoku są obiektami. Odpowiedniej klasy.

Ale nawet wtedy nigdy tak naprawdę nie dowiesz się, że każda funkcja widoku jest poprawna bez zestawu testów jednostkowych.


5
Nie mogę wygrać? Ale muszę wygrać! Przegrana nie wchodzi w grę! Ale poważnie, nie staram się unikać deklarowania wymagań dotyczących autoryzacji. Chcę tylko cofnąć to, co należy zadeklarować. Zamiast deklarować wszystkie widoki prywatne i nic nie mówić o widokach publicznych, chcę zadeklarować wszystkie widoki publiczne i ustawić domyślne jako prywatne.
samtregar

Poza tym fajny pomysł na widoki jako klasy ... Ale myślę, że przepisanie setek widoków w mojej aplikacji w tym momencie prawdopodobnie nie jest początkiem.
samtregar

@samtregar: Musisz wygrać? Muszę mieć nowego Bentleya. Poważnie. Możesz grepować za def. Możesz w trywialny sposób napisać bardzo krótki skrypt, aby przeskanować wszystko defwe wszystkich modułach widoku i określić, czy zapomniano o wpisie @login_required.
S.Lott,

8
@ S.Lott To najtrudniejszy możliwy sposób na zrobienie tego, ale tak, myślę, że to zadziała. Poza tym, skąd wiesz, które z nich są poglądami? Samo spojrzenie na funkcje w views.py nie zadziała, pomocnicze funkcje współdzielone nie wymagają @login_required.
samtregar

Tak, to kiepskie. Prawie najgorsze, jakie mogłem wymyślić. Nie wiesz, które definicje są widokami, z wyjątkiem zbadania urls.py.
S.Lott


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.