Il modo migliore per impostare il login_required di Django come predefinito


103

Sto lavorando a una grande app Django, la maggior parte della quale richiede un login per accedere. Ciò significa che in tutta la nostra app abbiamo cosparso:

@login_required
def view(...):

Va bene e funziona benissimo fintanto che ci ricordiamo di aggiungerlo ovunque ! Purtroppo a volte ci dimentichiamo e il fallimento spesso non è particolarmente evidente. Se l'unico collegamento a una visualizzazione è su una pagina @login_required, è improbabile che tu possa notare che puoi effettivamente raggiungere quella visualizzazione senza effettuare il login. Ma i malintenzionati potrebbero notare, il che è un problema.

La mia idea era di invertire il sistema. Invece di dover digitare @login_required ovunque, invece avrei qualcosa del tipo:

@public
def public_view(...):

Solo per cose pubbliche. Ho provato a implementarlo con un middleware e non riuscivo a farlo funzionare. Tutto ciò che ho provato ha interagito male con altri middleware che stiamo utilizzando, credo. Successivamente ho provato a scrivere qualcosa per attraversare i pattern URL per verificare che tutto ciò che non è @public fosse contrassegnato con @login_required - almeno allora avremmo ricevuto un rapido errore se ci fossimo dimenticati qualcosa. Ma poi non sono riuscito a capire come capire se @login_required fosse stato applicato a una vista ...

Allora, qual è il modo giusto per farlo? Grazie per l'aiuto!


2
Ottima domanda. Sono stato esattamente nella stessa posizione. Abbiamo middleware per rendere l' intero sito login_required e abbiamo una sorta di ACL sviluppato internamente per mostrare viste / frammenti di modelli diversi a persone / ruoli diversi, ma questo è diverso da entrambi.
Peter Rowell

Risposte:


99

Il middleware potrebbe essere la soluzione migliore. Ho usato questo pezzo di codice in passato, modificato da uno snippet trovato altrove:

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

Quindi in settings.py, elenca gli URL di base che desideri proteggere:

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

Finché il tuo sito segue le convenzioni URL per le pagine che richiedono l'autenticazione, questo modello funzionerà. Se non si tratta di un adattamento uno a uno, puoi scegliere di modificare il middleware in base alle tue circostanze.

Quello che mi piace di questo approccio - oltre a rimuovere la necessità di sporcare la base di codice con i @login_requireddecoratori - è che se lo schema di autenticazione cambia, hai un posto dove andare per apportare modifiche globali.


Grazie, sembra fantastico! Non mi è venuto in mente di utilizzare effettivamente login_required () nel mio middleware. Penso che questo aiuterà a risolvere il problema che stavo avendo giocando bene con il nostro stack middleware.
samtregar

Doh! Questo è quasi esattamente il modello che abbiamo utilizzato per un gruppo di pagine che dovevano essere HTTPS e tutto il resto non deve essere HTTPS. Era successo 2,5 anni fa e me ne ero completamente dimenticato. Grazie, Daniel!
Peter Rowell

4
La classe middleware RequireLoginMiddleware deve essere posizionata dove? views.py, models.py?
Yasin

1
@richard decorators viene eseguito in fase di compilazione, e in questo caso tutto ciò che ho fatto è stato: function.public = True. Quindi, quando il middleware viene eseguito, può cercare il flag .public sulla funzione per decidere se consentire o meno l'accesso. Se ciò non ha senso, posso inviarti il ​​codice completo.
samtregar

1
Penso che l'approccio migliore sia creare @publicdecorator, che imposta l' _publicattributo in vista, e il middleware quindi salta quelle viste. Il decoratore csrf_exempt di Django funziona allo stesso modo
Ivan Virabyan

31

Esiste un'alternativa all'inserimento di un decoratore su ciascuna funzione di visualizzazione. Puoi anche inserire il login_required()decoratore nel urls.pyfile. Sebbene questa sia ancora un'attività manuale, almeno hai tutto in un unico posto, il che rende più facile l'audit.

per esempio,

    da my_views importa home_view

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

Notare che le funzioni di visualizzazione vengono denominate e importate direttamente, non come stringhe.

Si noti inoltre che funziona con qualsiasi oggetto di visualizzazione richiamabile, comprese le classi.


3

In Django 2.1, possiamo decorare tutti i metodi in una classe con:

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'

AGGIORNAMENTO: Ho anche scoperto che funziona:

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

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

e imposta LOGIN_URL = '/accounts/login/'nel tuo settings.py


1
grazie per questa nuova risposta. ma per favore spiegami un po ', non sono riuscito a capirlo anche se ho letto il documento ufficiale. grazie per il tuo aiuto in anticipo
Tian Loon

@TianLoon per favore guarda la mia risposta aggiornata, potrebbe aiutare.
andyandy

2

È difficile cambiare i presupposti incorporati in Django senza rielaborare il modo in cui gli URL vengono distribuiti per visualizzare le funzioni.

Invece di perdere tempo negli interni di Django, ecco un audit che puoi usare. Controlla semplicemente ogni funzione di visualizzazione.

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

Eseguilo ed esamina l'output di defs senza decoratori appropriati.


2

Ecco una soluzione middleware per django 1.10+

I middleware devono essere scritti in un modo nuovo in django 1.10+ .

Codice

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

Installazione

  1. Copia il codice nella cartella del progetto e salva come middleware.py
  2. Aggiungi a MIDDLEWARE

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Richiedi login]

  3. Aggiungi a settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

fonti:

  1. Questa risposta di Daniel Naab

  2. Django Middleware tutorial di Max Goodridge

  3. Django Middleware Docs


Nota che sebbene non accada nulla __call__, il process_viewgancio è ancora utilizzato [modificato]
Simon Kohlmeyer,

1

Ispirato dalla risposta di Ber, ho scritto un piccolo snippet che sostituisce la patternsfunzione, avvolgendo tutti i callback URL con il login_requireddecoratore. Funziona 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

Usarlo funziona in questo modo (la chiamata a listè richiesta a causa di yield).

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

0

Non puoi davvero vincere questo. È semplicemente necessario fare una dichiarazione dei requisiti di autorizzazione. In quale altro luogo metteresti questa dichiarazione se non proprio dalla funzione di visualizzazione?

Valuta la possibilità di sostituire le funzioni di visualizzazione con oggetti richiamabili.

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

Quindi crei le tue sottoclassi di funzioni di visualizzazione LoginViewFunction.

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

my_real_view = MyRealView()  

Non salva nessuna riga di codice. E non aiuta il problema del "ci siamo dimenticati". Tutto quello che puoi fare è esaminare il codice per assicurarti che le funzioni di visualizzazione siano oggetti. Della classe giusta.

Ma anche allora, non saprai mai veramente che ogni funzione di visualizzazione è corretta senza una suite di unit test.


5
Non posso vincere? Ma devo vincere! Perdere non è un'opzione! Ma seriamente, non sto cercando di evitare di dichiarare i miei requisiti di autenticazione. Voglio solo annullare ciò che deve essere dichiarato. Invece di dover dichiarare tutte le viste private e non parlare di quelle pubbliche, desidero dichiarare tutte le viste pubbliche e fare in modo che l'impostazione predefinita sia privata.
samtregar

Inoltre, un'idea chiara per le visualizzazioni come classi ... Ma penso che riscrivere le centinaia di visualizzazioni nella mia app a questo punto sia probabilmente un non-principiante.
samtregar

@samtregar: devi vincere? Devo avere una nuova Bentley. Sul serio. Puoi grep per def's. Puoi scrivere banalmente uno script molto breve per scansionare tutto defin tutti i moduli di visualizzazione e determinare se un @login_required è stato dimenticato.
S.Lott

8
@ S.Lott Questo è il modo più debole possibile per farlo, ma sì, immagino che funzionerebbe. Tranne come fai a sapere quali def sono le visualizzazioni? Basta guardare le funzioni in views.py non funzionerà, le funzioni condivise di supporto non hanno bisogno di @login_required.
samtregar

Sì, è zoppo. Quasi il più stupido a cui potessi pensare. Non sai quali definizioni sono viste se non esaminando il file urls.py.
S.Lott


0

C'è un'app che fornisce una soluzione plug-and-play a questo:

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

pip install django-stronghold
# settings.py

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

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.