Meilleur moyen de définir le login_required de Django comme valeur par défaut


103

Je travaille sur une grande application Django, dont la grande majorité nécessite une connexion pour y accéder. Cela signifie que tout au long de notre application, nous avons saupoudré:

@login_required
def view(...):

C'est très bien, et cela fonctionne très bien tant que nous nous souvenons de l'ajouter partout ! Malheureusement, parfois, nous oublions, et l'échec n'est souvent pas très évident. Si le seul lien vers une vue se trouve sur une page @login_required, vous ne remarquerez probablement pas que vous pouvez réellement accéder à cette vue sans vous connecter. Mais les méchants peuvent remarquer, ce qui pose problème.

Mon idée était d'inverser le système. Au lieu d'avoir à taper @login_required partout, j'aurais plutôt quelque chose comme:

@public
def public_view(...):

Juste pour les trucs publics. J'ai essayé de l'implémenter avec un middleware et je n'arrivais pas à le faire fonctionner. Tout ce que j'ai essayé a mal interagi avec les autres intergiciels que nous utilisons, je pense. Ensuite, j'ai essayé d'écrire quelque chose pour parcourir les modèles d'URL pour vérifier que tout ce qui n'est pas @public était marqué @login_required - au moins, nous aurions une erreur rapide si nous oublions quelque chose. Mais ensuite, je ne savais pas comment savoir si @login_required avait été appliqué à une vue ...

Alors, quelle est la bonne façon de faire cela? Merci pour l'aide!


2
Excellente question. J'ai été exactement dans la même position. Nous avons un middleware pour rendre l' ensemble du site login_required, et nous avons une sorte d'ACL maison pour montrer différentes vues / fragments de modèles à différentes personnes / rôles, mais cela est différent de l'un ou l'autre de ceux-ci.
Peter Rowell

Réponses:


99

L'intergiciel peut être votre meilleur pari. J'ai utilisé ce morceau de code dans le passé, modifié à partir d'un extrait trouvé ailleurs:

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

Ensuite, dans settings.py, répertoriez les URL de base que vous souhaitez protéger:

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

Tant que votre site respecte les conventions d'URL pour les pages nécessitant une authentification, ce modèle fonctionnera. S'il ne s'agit pas d'un ajustement individuel, vous pouvez choisir de modifier le middleware pour mieux l'adapter à votre situation.

Ce que j'aime dans cette approche - en plus de supprimer la nécessité de joncher la base de code avec des @login_requireddécorateurs - c'est que si le schéma d'authentification change, vous avez un endroit où aller pour apporter des changements globaux.


Merci, ça a l'air génial! Il ne m'est pas venu à l'esprit d'utiliser réellement login_required () dans mon middleware. Je pense que cela aidera à contourner le problème que j'avais avec notre pile middleware.
samtregar

Doh! C'est presque exactement le modèle que nous avons utilisé pour un groupe de pages qui devait être HTTPS, et tout le reste ne doit pas être HTTPS. C'était il y a 2,5 ans et je l'avais complètement oublié. Merci, Daniel!
Peter Rowell

4
La classe middleware RequireLoginMiddleware doit être placée où? views.py, models.py?
Yasin

1
Les décorateurs @richard fonctionnent au moment de la compilation, et dans ce cas, tout ce que j'ai fait était: function.public = True. Ensuite, lorsque le middleware s'exécute, il peut rechercher l'indicateur .public sur la fonction pour décider d'autoriser ou non l'accès. Si cela n'a pas de sens, je peux vous envoyer le code complet.
samtregar

1
Je pense que la meilleure approche est de créer un @publicdécorateur, qui définit l' _publicattribut sur la vue, et le middleware ignore ensuite ces vues. Le décorateur csrf_exempt de Django fonctionne de la même manière
Ivan Virabyan

31

Il existe une alternative à la mise en place d'un décorateur sur chaque fonction de vue. Vous pouvez également mettre le login_required()décorateur dans le urls.pyfichier. Bien qu'il s'agisse encore d'une tâche manuelle, au moins vous avez tout cela au même endroit, ce qui facilite l'audit.

par exemple,

    depuis my_views import home_view

    urlpatterns = modèles ('',
        # "Accueil":
        (r '^ $', login_required (home_view), dict (template_name = 'my_site / home.html', items_per_page = 20)),
    )

Notez que les fonctions d'affichage sont nommées et importées directement, pas sous forme de chaînes.

Notez également que cela fonctionne avec n'importe quel objet de vue appelable, y compris les classes.


3

Dans Django 2.1, nous pouvons décorer toutes les méthodes d'une classe avec:

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'

MISE À JOUR: J'ai également trouvé ce qui suit pour fonctionner:

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

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

et définissez-le LOGIN_URL = '/accounts/login/'dans vos paramètres.py


1
merci pour cette nouvelle réponse. mais s'il vous plaît expliquer un peu à ce sujet, je ne pourrais pas l'obtenir même si j'ai lu la doc officielle. merci pour votre aide à l'avance
Tian Loon

@TianLoon s'il vous plaît voir ma réponse mise à jour, cela peut aider.
andyandy

2

Il est difficile de changer les hypothèses intégrées dans Django sans retravailler la façon dont les URL sont transmises pour afficher les fonctions.

Au lieu de vous amuser dans les internes de Django, voici un audit que vous pouvez utiliser. Vérifiez simplement chaque fonction d'affichage.

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

Exécutez ceci et examinez la sortie pour defs sans décorateurs appropriés.


2

Voici une solution middleware pour django 1.10+

Les middlewares doivent être écrits d'une nouvelle manière dans django 1.10+ .

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. Copiez le code dans votre dossier de projet et enregistrez-le sous middleware.py
  2. Ajouter au MIDDLEWARE

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Requiert une connexion]

  3. Ajoutez à vos settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Sources:

  1. Cette réponse de Daniel Naab

  2. Tutoriel Django Middleware par Max Goodridge

  3. Documentation du middleware Django


Notez que bien que rien ne se passe __call__, le process_viewcrochet est toujours utilisé [édité]
Simon Kohlmeyer

1

Inspiré par la réponse de Ber, j'ai écrit un petit extrait de code qui remplace la patternsfonction, en enveloppant tous les rappels d'URL avec le login_requireddécorateur. Cela fonctionne dans 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

Son utilisation fonctionne comme ceci (l'appel à listest requis à cause du yield).

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

0

Vous ne pouvez pas vraiment gagner ça. Vous devez simplement faire une déclaration des conditions d'autorisation. Où mettriez-vous cette déclaration sauf juste à côté de la fonction view?

Envisagez de remplacer vos fonctions d'affichage par des objets appelables.

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

Vous faites ensuite de vos fonctions de vue des sous-classes de LoginViewFunction.

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

my_real_view = MyRealView()  

Il n'enregistre aucune ligne de code. Et cela n'aide pas le problème «nous avons oublié». Tout ce que vous pouvez faire est d'examiner le code pour vous assurer que les fonctions d'affichage sont des objets. De la bonne classe.

Mais même dans ce cas, vous ne saurez jamais vraiment que chaque fonction d'affichage est correcte sans suite de tests unitaires.


5
Je ne peux pas gagner? Mais je dois gagner! Perdre n'est pas une option! Mais sérieusement, je n'essaye pas d'éviter de déclarer mes conditions d'authentification. Je veux juste inverser ce qui doit être déclaré. Au lieu d'avoir à déclarer toutes les vues privées et à ne rien dire sur les vues publiques, je veux déclarer toutes les vues publiques et que la valeur par défaut soit privée.
samtregar

Aussi, bonne idée pour les vues en tant que classes ... Mais je pense que réécrire les centaines de vues dans mon application à ce stade est probablement un non-démarreur.
samtregar

@samtregar: Vous devez gagner? Je dois avoir une nouvelle Bentley. Sérieusement. Vous pouvez grep pour def's. Vous pouvez écrire un script très court pour analyser tous defles modules de vue et déterminer si un @login_required a été oublié.
S.Lott

8
@ S.Lott C'est la façon la plus louche de faire ça, mais oui, je suppose que ça marcherait. Sauf comment savoir quelles définitions sont des vues? Le simple fait de regarder les fonctions dans views.py ne fonctionnera pas, les fonctions d'aide partagées là-bas n'ont pas besoin de @login_required.
samtregar

Oui, c'est nul. Presque le plus faible auquel je puisse penser. Vous ne savez pas quelles définitions sont des vues, sauf en examinant le fichier urls.py.
S.Lott


0

Il existe une application qui fournit une solution plug-and-play à cela:

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

pip install django-stronghold
# settings.py

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

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.