La mejor manera de hacer que el login_required de Django sea el predeterminado

103

Estoy trabajando en una gran aplicación de Django, la gran mayoría de las cuales requiere un inicio de sesión para acceder. Esto significa que en toda nuestra aplicación hemos rociado:

@login_required
def view(...):

Está bien y funciona muy bien siempre que recordemos agregarlo en todas partes . Lamentablemente, a veces lo olvidamos, y el fracaso a menudo no es tan evidente. Si el único enlace a una vista está en una página @login_required, entonces no es probable que notes que puedes llegar a esa vista sin iniciar sesión. Pero los malos pueden notarlo, lo cual es un problema.

Mi idea era revertir el sistema. En lugar de tener que escribir @login_required en todas partes, tendría algo como:

@public
def public_view(...):

Solo para las cosas públicas. Intenté implementar esto con algo de middleware y parecía que no podía hacerlo funcionar. Todo lo que probé interactuó mal con otros middleware que estamos usando, creo. A continuación, intenté escribir algo para recorrer los patrones de URL para comprobar que todo lo que no es @public estaba marcado como @login_required; al menos, obtendríamos un error rápido si olvidábamos algo. Pero luego no pude averiguar cómo saber si @login_required se había aplicado a una vista ...

Entonces, ¿cuál es la forma correcta de hacer esto? ¡Gracias por la ayuda!

Samtregar
fuente
2
Excelente pregunta. He estado exactamente en la misma posición. Tenemos middleware para hacer que todo el sitio sea necesario para iniciar sesión, y tenemos un tipo de ACL de cosecha propia para mostrar diferentes vistas / fragmentos de plantilla a diferentes personas / roles, pero esto es diferente de cualquiera de ellos.
Peter Rowell

Respuestas:

99

El middleware puede ser su mejor opción. He usado este fragmento de código en el pasado, modificado de un fragmento que se encuentra en otro lugar:

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

Luego, en settings.py, enumere las URL base que desea proteger:

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

Siempre que su sitio siga las convenciones de URL para las páginas que requieren autenticación, este modelo funcionará. Si esto no es un ajuste uno a uno, puede optar por modificar el middleware para que se adapte más a sus circunstancias.

Lo que me gusta de este enfoque, además de eliminar la necesidad de ensuciar el código base con @login_requireddecoradores, es que si el esquema de autenticación cambia, tiene un lugar adonde ir para realizar cambios globales.

Daniel Naab
fuente
Gracias, ¡esto se ve genial! No se me ocurrió usar login_required () en mi middleware. Creo que esto ayudará a solucionar el problema que estaba teniendo jugando bien con nuestra pila de middleware.
samtregar
Doh! Este es casi exactamente el patrón que usamos para un grupo de páginas que tenían que ser HTTPS, y todo lo demás no debe ser HTTPS. Eso fue hace dos años y medio y lo había olvidado por completo. ¡Gracias, Daniel!
Peter Rowell
4
¿Dónde debería colocarse la clase de middleware RequireLoginMiddleware? views.py, models.py?
Yasin
1
Los decoradores de @richard se ejecutan en tiempo de compilación, y en este caso todo lo que hice fue: function.public = True. Luego, cuando se ejecuta el middleware, puede buscar el indicador .public en la función para decidir si permite el acceso o no. Si eso no tiene sentido, puedo enviarle el código completo.
samtregar
1
Creo que el mejor enfoque es hacer @publicdecorador, que establece el _publicatributo a la vista, y el middleware luego omite esas vistas. El decorador csrf_exempt de Django funciona de la misma manera
Ivan Virabyan
31

Existe una alternativa a poner un decorador en cada función de vista. También puede poner el login_required()decorador en el urls.pyarchivo. Si bien esto sigue siendo una tarea manual, al menos lo tiene todo en un solo lugar, lo que facilita la auditoría.

p.ej,

    de my_views importar home_view

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

Tenga en cuenta que las funciones de vista se nombran y se importan directamente, no como cadenas.

También tenga en cuenta que esto funciona con cualquier objeto de vista invocable, incluidas las clases.

Ber
fuente
3

En Django 2.1, podemos decorar todos los métodos en una clase 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'

ACTUALIZACIÓN: También he encontrado que lo siguiente funciona:

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

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

y establezca LOGIN_URL = '/accounts/login/'en su settings.py

andyandy
fuente
1
gracias por esta nueva respuesta. pero por favor explique un poco al respecto, no pude conseguirlo incluso si leí el documento oficial. gracias por su ayuda de antemano
Tian Loon
@TianLoon, por favor vea mi respuesta actualizada, puede ayudar.
andyandy
2

Es difícil cambiar las suposiciones incorporadas en Django sin reelaborar la forma en que se entregan las URL para ver las funciones.

En lugar de perder el tiempo en el interior de Django, aquí hay una auditoría que puede usar. Simplemente verifique cada función de vista.

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

Ejecute esto y examine la salida para defs sin los decoradores adecuados.

S.Lott
fuente
2

Aquí hay una solución de middleware para django 1.10+

Los middlewares deben escribirse de una manera nueva en django 1.10+ .

Código

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

Instalación

  1. Copie el código en la carpeta de su proyecto y guárdelo como middleware.py
  2. Agregar a MIDDLEWARE

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Requerir inicio de sesión]

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

Fuentes:

  1. Esta respuesta de Daniel Naab

  2. Tutorial de Django Middleware por Max Goodridge

  3. Documentos de middleware de Django

np8
fuente
Tenga en cuenta que aunque no sucede nada en __call__, el process_viewgancho todavía se usa [editado]
Simon Kohlmeyer
1

Inspirado por la respuesta de Ber, escribí un pequeño fragmento que reemplaza la patternsfunción, envolviendo todas las devoluciones de llamada de URL con el login_requireddecorador. Esto funciona en 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

Su uso funciona así (la llamada a listes necesaria debido a yield).

urlpatterns = list(login_required_patterns('', url(r'^$', home_view)))
rectángulo
fuente
0

Realmente no puedes ganar esto. Simplemente debe hacer una declaración de los requisitos de autorización. ¿En qué otro lugar pondría esta declaración excepto junto a la función de vista?

Considere reemplazar sus funciones de vista con objetos invocables.

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

Luego, convierte sus funciones de vista en subclases de LoginViewFunction.

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

my_real_view = MyRealView()  

No guarda ninguna línea de código. Y no ayuda al problema de "nos olvidamos". Todo lo que puede hacer es examinar el código para asegurarse de que las funciones de vista sean objetos. De la clase adecuada.

Pero incluso entonces, nunca sabrá realmente que todas las funciones de visualización son correctas sin un conjunto de pruebas unitarias.

S.Lott
fuente
5
No puedo ganar ¡Pero tengo que ganar! ¡Perder no es una opcion! Pero en serio, no estoy tratando de evitar declarar mis requisitos de autorización. Solo quiero revertir lo que se debe declarar. En lugar de tener que declarar todas las vistas privadas y no decir nada sobre las públicas, quiero declarar todas las vistas públicas y que la predeterminada sea privada.
samtregar
Además, una buena idea para las vistas como clases ... Pero creo que reescribir los cientos de vistas en mi aplicación en este punto probablemente no sea un comienzo.
samtregar
@samtregar: ¿Tienes que ganar? Tengo que tener un Bentley nuevo. Seriamente. Puede grep para def's. Puede escribir trivialmente un script muy corto para escanear todos deflos módulos de vista y determinar si se olvidó un @login_required.
S.Lott
8
@ S.Lott Esa es la forma más tonta de hacer esto, pero sí, supongo que funcionaría. Excepto, ¿cómo saber qué defs son vistas? Solo mirar las funciones en views.py no funcionará, las funciones compartidas de ayuda allí no necesitan @login_required.
samtregar
Sí, es patético. Casi el más tonto que pude pensar. No sabe qué defs son vistas excepto al examinar el archivo urls.py.
S.Lott
0

Hay una aplicación que proporciona una solución plug-and-play para esto:

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

pip install django-stronghold
# settings.py

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

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