Der beste Weg, um Djangos login_required zum Standard zu machen

103

Ich arbeite an einer großen Django-App, für deren Zugriff zum größten Teil ein Login erforderlich ist. Dies bedeutet, dass wir in unserer gesamten App Folgendes verteilt haben:

@login_required
def view(...):

Das ist in Ordnung und funktioniert hervorragend , solange wir daran denken, es überall hinzuzufügen ! Leider vergessen wir manchmal, und der Fehler ist oft nicht besonders offensichtlich. Wenn sich der einzige Link zu einer Ansicht auf einer @ login_required-Seite befindet, werden Sie wahrscheinlich nicht bemerken, dass Sie diese Ansicht tatsächlich erreichen können, ohne sich anzumelden. Aber die Bösen bemerken möglicherweise, was ein Problem ist.

Meine Idee war es, das System umzukehren. Anstatt überall @login_required eingeben zu müssen, hätte ich stattdessen etwas wie:

@public
def public_view(...):

Nur für die Öffentlichkeit. Ich habe versucht, dies mit Middleware zu implementieren, und ich konnte es anscheinend nicht zum Laufen bringen. Alles, was ich versucht habe, hat schlecht mit anderer Middleware interagiert, die wir verwenden, denke ich. Als nächstes habe ich versucht, etwas zu schreiben, um die URL-Muster zu durchlaufen, um zu überprüfen, ob alles, was nicht @public ist, als @login_required markiert ist - zumindest dann würden wir einen schnellen Fehler erhalten, wenn wir etwas vergessen. Aber dann konnte ich nicht herausfinden, ob @login_required auf eine Ansicht angewendet wurde ...

Also, was ist der richtige Weg, um dies zu tun? Danke für die Hilfe!

samtregar
quelle
2
Ausgezeichnete Frage. Ich war in genau der gleichen Position. Wir haben Middleware, mit der die gesamte Site login_required erstellt werden kann, und wir haben eine eigene ACL, mit der verschiedene Ansichten / Vorlagenfragmente verschiedenen Personen / Rollen angezeigt werden können. Dies unterscheidet sich jedoch von beiden.
Peter Rowell

Antworten:

99

Middleware ist möglicherweise die beste Wahl. Ich habe diesen Code in der Vergangenheit verwendet, modifiziert von einem Snippet, das an anderer Stelle gefunden wurde:

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

Listen Sie dann in settings.py die Basis-URLs auf, die Sie schützen möchten:

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

Solange Ihre Site den URL-Konventionen für die Seiten entspricht, für die eine Authentifizierung erforderlich ist, funktioniert dieses Modell. Wenn dies keine Eins-zu-Eins-Anpassung ist, können Sie die Middleware an Ihre Umstände anpassen.

Was mir an diesem Ansatz gefällt - abgesehen davon, dass die Codebasis nicht mehr mit @login_requiredDekorateuren übersät werden muss - ist, dass Sie einen Ort haben, an dem Sie globale Änderungen vornehmen können, wenn sich das Authentifizierungsschema ändert.

Daniel Naab
quelle
Danke, das sieht gut aus! Es ist mir nicht in den Sinn gekommen, login_required () in meiner Middleware zu verwenden. Ich denke, dies wird helfen, das Problem zu umgehen, das ich mit unserem Middleware-Stack hatte.
Samtregar
Doh! Dies ist fast genau das Muster, das wir für eine Gruppe von Seiten verwendet haben, die HTTPS sein mussten, und alles andere darf nicht HTTPS sein. Das war vor 2,5 Jahren und ich hatte es völlig vergessen. Danke, Daniel!
Peter Rowell
4
Die Middleware RequireLoginMiddleware-Klasse sollte wo platziert werden? views.py, models.py?
Yasin
1
@richard Dekorateure werden zur Kompilierungszeit ausgeführt, und in diesem Fall habe ich nur Folgendes ausgeführt: function.public = True. Wenn die Middleware ausgeführt wird, kann sie nach dem Flag .public in der Funktion suchen, um zu entscheiden, ob der Zugriff zugelassen werden soll oder nicht. Wenn das keinen Sinn ergibt, kann ich Ihnen den vollständigen Code senden.
Samtregar
1
Ich denke, der beste Ansatz ist es, einen @publicDekorator zu erstellen, der das _publicAttribut für die Ansicht festlegt , und die Middleware überspringt diese Ansichten dann. Djangos Dekorateur csrf_exempt funktioniert genauso
Ivan Virabyan
31

Es gibt eine Alternative zum Dekorieren jeder Ansichtsfunktion. Sie können den login_required()Dekorateur auch in die urls.pyDatei einfügen. Obwohl dies noch eine manuelle Aufgabe ist, haben Sie zumindest alles an einem Ort, was die Prüfung erleichtert.

z.B,

    aus my_views importiere home_view

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

Beachten Sie, dass Ansichtsfunktionen direkt benannt und importiert werden, nicht als Zeichenfolgen.

Beachten Sie auch, dass dies mit jedem aufrufbaren Ansichtsobjekt funktioniert, einschließlich Klassen.

Ber
quelle
3

In Django 2.1 können wir alle Methoden in einer Klasse dekorieren mit:

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'

UPDATE: Ich habe auch festgestellt, dass Folgendes funktioniert:

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

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

und LOGIN_URL = '/accounts/login/'in Ihrer settings.py einstellen

andyandy
quelle
1
Danke für diese neue Antwort. aber bitte erkläre ein bisschen darüber, ich könnte es nicht bekommen, selbst wenn ich das offizielle Dokument lese. Vielen Dank für Ihre Hilfe im Voraus
Tian Loon
@TianLoon siehe meine aktualisierte Antwort, es kann helfen.
Andyyy
2

Es ist schwierig, die in Django integrierten Annahmen zu ändern, ohne die Art und Weise zu überarbeiten, wie URLs zum Anzeigen von Funktionen übergeben werden.

Anstatt in Django-Interna herumzuspielen, können Sie hier ein Audit verwenden. Überprüfen Sie einfach jede Ansichtsfunktion.

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

Führen Sie dies aus und überprüfen Sie die Ausgabe auf defs ohne geeignete Dekoratoren.

S.Lott
quelle
2

Hier ist eine Middleware-Lösung für Django 1.10+

Die Middlewares in müssen in Django 1.10+ neu geschrieben werden .

Code

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

Installation

  1. Kopieren Sie den Code in Ihren Projektordner und speichern Sie ihn als middleware.py
  2. Zu MIDDLEWARE hinzufügen

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Login erforderlich]

  3. Zu Ihrer settings.py hinzufügen:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Quellen:

  1. Diese Antwort von Daniel Naab

  2. Django Middleware Tutorial von Max Goodridge

  3. Django Middleware Docs

np8
quelle
Beachten Sie, dass, obwohl nichts passiert __call__, der process_viewHaken immer noch verwendet wird [bearbeitet]
Simon Kohlmeyer
1

Inspiriert von Ber's Antwort schrieb ich einen kleinen Ausschnitt, der die patternsFunktion ersetzt, indem ich alle URL-Rückrufe mit dem login_requiredDekorateur umwickelte . Dies funktioniert in 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

Die Verwendung funktioniert folgendermaßen (der Aufruf von listist aufgrund der erforderlich yield).

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

Das kann man nicht wirklich gewinnen. Sie müssen lediglich eine Erklärung der Autorisierungsanforderungen abgeben. Wo sonst würden Sie diese Deklaration platzieren, außer direkt neben der Ansichtsfunktion?

Ersetzen Sie Ihre Ansichtsfunktionen durch aufrufbare Objekte.

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

Anschließend machen Sie Ihre Ansichtsfunktionen zu Unterklassen von LoginViewFunction.

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

my_real_view = MyRealView()  

Es werden keine Codezeilen gespeichert. Und es hilft nicht dem Problem "Wir haben vergessen". Sie können lediglich den Code untersuchen, um sicherzustellen, dass die Ansichtsfunktionen Objekte sind. Von der richtigen Klasse.

Aber selbst dann werden Sie nie wirklich wissen, dass jede Ansichtsfunktion ohne eine Unit-Test-Suite korrekt ist.

S.Lott
quelle
5
Ich kann nicht gewinnen Aber ich muss gewinnen! Verlieren ist keine Option! Aber im Ernst, ich versuche nicht zu vermeiden, meine Authentifizierungsanforderungen zu deklarieren. Ich möchte nur umkehren, was deklariert werden muss. Anstatt alle privaten Ansichten deklarieren zu müssen und nichts über öffentliche Ansichten zu sagen, möchte ich alle öffentlichen Ansichten deklarieren und die Standardeinstellung privat sein.
Samtregar
Auch eine nette Idee für Ansichten als Klassen ... Aber ich denke, das Umschreiben der Hunderte von Ansichten in meiner App zu diesem Zeitpunkt ist wahrscheinlich kein Anfänger.
Samtregar
@samtregar: Du musst gewinnen? Ich muss einen neuen Bentley haben. Ernsthaft. Sie können nach def's greifen . Sie können trivial ein sehr kurzes Skript schreiben, um alle defin allen Ansichtsmodulen zu scannen und festzustellen, ob ein @login_required vergessen wurde.
S.Lott
8
@ S.Lott Das ist der lahmste Weg dies zu tun, aber ja, ich denke es würde funktionieren. Außer woher weißt du, welche Defs Ansichten sind? Nur das Betrachten von Funktionen in views.py funktioniert nicht. Für freigegebene Helferfunktionen ist @login_required nicht erforderlich.
Samtregar
Ja, es ist lahm. Fast das lahmste, an das ich denken konnte. Sie wissen nicht, welche Defs Ansichten sind, außer indem Sie die untersuchen urls.py.
S.Lott
0

Es wäre möglich, einen einzigen Ausgangspunkt für alle urlsin einer Art Include zu haben, der ihn mit diesen Paketen https://github.com/vorujack/decorate_url dekoriert .

rootart
quelle
0

Es gibt eine App, die eine Plug-and-Play-Lösung dafür bietet:

https://github.com/mgrouchy/django-stronghold

pip install django-stronghold
# settings.py

INSTALLED_APPS = (
    #...
    'stronghold',
)

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
getup8
quelle