Melhor maneira de tornar o login_required do Django o padrão

103

Estou trabalhando em um grande aplicativo Django, a grande maioria dos quais requer um login para acessar. Isso significa que em todo o nosso aplicativo espalhamos:

@login_required
def view(...):

Tudo bem e funciona muito bem , desde que nos lembremos de adicioná-lo em todos os lugares ! Infelizmente, às vezes esquecemos, e a falha muitas vezes não é terrivelmente evidente. Se o único link para uma visualização estiver em uma página @login_required, você provavelmente não perceberá que pode realmente acessar essa visualização sem fazer login. Mas os bandidos podem perceber, o que é um problema.

Minha ideia era reverter o sistema. Em vez de digitar @login_required em todos os lugares, eu teria algo como:

@public
def public_view(...):

Apenas para as coisas públicas. Tentei implementar isso com algum middleware e não consegui fazer funcionar. Tudo o que experimentei interagiu mal com outro middleware que estamos usando, eu acho. Em seguida, tentei escrever algo para percorrer os padrões de URL para verificar se tudo o que não é @public foi marcado como @login_required - pelo menos, obteríamos um erro rápido se esquecêssemos algo. Mas não consegui descobrir como saber se @login_required tinha sido aplicado a uma visualização ...

Então, qual é a maneira certa de fazer isso? Obrigado pela ajuda!

samtregar
fonte
2
Excelente pergunta. Eu estive exatamente na mesma posição. Temos middleware para tornar o site login_required inteiro e temos uma espécie de ACL criada internamente para mostrar diferentes visualizações / fragmentos de modelo para diferentes pessoas / funções, mas isso é diferente de qualquer um deles.
Peter Rowell

Respostas:

99

Middleware pode ser sua melhor aposta. Eu usei este trecho de código no passado, modificado de um snippet encontrado em outro 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

Em seguida, em settings.py, liste os URLs básicos que deseja proteger:

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

Desde que o seu site siga as convenções de URL para as páginas que requerem autenticação, este modelo funcionará. Se não for um ajuste individual, você pode escolher modificar o middleware para se adequar mais às suas circunstâncias.

O que eu gosto nessa abordagem - além de remover a necessidade de entupir a base de código com @login_requireddecoradores - é que, se o esquema de autenticação mudar, você terá um lugar para ir para fazer mudanças globais.

Daniel Naab
fonte
Obrigado, isso parece ótimo! Não me ocorreu usar login_required () em meu middleware. Acho que isso vai ajudar a contornar o problema que estava tendo para jogar bem com nossa pilha de middleware.
samtregar
Doh! Esse é quase exatamente o padrão que usamos para um grupo de páginas que tinha que ser HTTPS e todo o resto não deve ser HTTPS. Isso foi há 2,5 anos e eu tinha me esquecido completamente disso. Obrigado, Daniel!
Peter Rowell
4
A classe RequireLoginMiddleware de middleware deve ser colocada onde? views.py, models.py?
Yasin de
1
Os decoradores @richard são executados em tempo de compilação e, neste caso, tudo o que fiz foi: function.public = True. Então, quando o middleware é executado, ele pode procurar o sinalizador .public na função para decidir se permite o acesso ou não. Se isso não fizer sentido, posso enviar o código completo.
samtregar
1
Acho que a melhor abordagem é fazer o @publicdecorator, que define o _publicatributo na visualização e o middleware, em seguida, pula essas visualizações. O decorador csrf_exempt do Django funciona da mesma maneira
Ivan Virabyan
31

Existe uma alternativa para colocar um decorador em cada função de visualização. Você também pode colocar o login_required()decorador no urls.pyarquivo. Embora ainda seja uma tarefa manual, pelo menos você tem tudo em um só lugar, o que facilita a auditoria.

por exemplo,

    de my_views importar home_view

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

Observe que as funções de visualização são nomeadas e importadas diretamente, não como strings.

Observe também que isso funciona com qualquer objeto de exibição que pode ser chamado, incluindo classes.

Ber
fonte
3

No Django 2.1, podemos decorar todos os métodos em uma classe com:

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'

ATUALIZAÇÃO: Eu também descobri que o seguinte funciona:

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

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

e definir LOGIN_URL = '/accounts/login/'em settings.py

andyandy
fonte
1
obrigado por esta nova resposta. mas, por favor, explique um pouco sobre isso, não consegui entender mesmo se eu li o documento oficial obrigado pela sua ajuda antecipadamente
Tian Loon
@TianLoon, veja minha resposta atualizada, pode ajudar.
andyandy
2

É difícil mudar as suposições embutidas no Django sem retrabalhar a maneira como as urls são entregues para as funções de visualização.

Em vez de mexer nos componentes internos do Django, aqui está uma auditoria que você pode usar. Basta verificar cada função de visualização.

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

Execute-o e examine a saída para defs sem decoradores apropriados.

S.Lott
fonte
2

Aqui está uma solução de middleware para django 1.10+

Os middlewares devem ser escritos de uma nova maneira no 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

Instalação

  1. Copie o código para a pasta do projeto e salve como middleware.py
  2. Adicionar ao MIDDLEWARE

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

  3. Adicione às suas configurações.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Fontes:

  1. Esta resposta de Daniel Naab

  2. Tutorial do Django Middleware por Max Goodridge

  3. Django Middleware Docs

np8
fonte
Observe que embora nada aconteça __call__, o process_viewgancho ainda é usado [editado]
Simon Kohlmeyer
1

Inspirado pela resposta de Ber, escrevi um pequeno snippet que substitui a patternsfunção, envolvendo todos os retornos de chamada de URL com o login_requireddecorador. Isso funciona no 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

Usá-lo funciona assim (a chamada para listé necessária por causa do yield).

urlpatterns = list(login_required_patterns('', url(r'^$', home_view)))
retângulo
fonte
0

Você realmente não pode vencer isso. Você simplesmente deve fazer uma declaração dos requisitos de autorização. Onde mais você colocaria esta declaração, exceto ao lado da função view?

Considere substituir suas funções de visualização por objetos que podem ser chamados.

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

Você então transforma suas funções de visualização em subclasses de LoginViewFunction.

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

my_real_view = MyRealView()  

Não salva nenhuma linha de código. E não ajuda o problema do "esquecemos". Tudo o que você pode fazer é examinar o código para ter certeza de que as funções de visualização são objetos. Da classe certa.

Mas mesmo assim, você nunca saberá realmente que todas as funções de visualização estão corretas sem um conjunto de testes de unidade.

S.Lott
fonte
5
Eu não posso vencer? Mas eu tenho que vencer! Perder não é uma opção! Mas, falando sério, não estou tentando evitar declarar meus requisitos de autenticação. Eu só quero reverter o que precisa ser declarado. Em vez de ter que declarar todas as visualizações privadas e não dizer nada sobre as públicas, quero declarar todas as visualizações públicas e ter o padrão privado.
samtregar
Além disso, uma ideia legal para visualizações como classes ... Mas eu acho que reescrever as centenas de visualizações em meu aplicativo neste ponto provavelmente é um fracasso.
samtregar
@samtregar: Você tem que vencer? Preciso de um novo Bentley. Seriamente. Você pode usar o grep para def's. Você pode escrever trivialmente um script muito curto para varrer todos defos módulos de visualização e determinar se um @login_required foi esquecido.
S.Lott
8
@ S.Lott Essa é a maneira mais idiota possível de fazer isso, mas sim, acho que funcionaria. Exceto como você sabe quais defs são visualizações? Apenas olhar para as funções em views.py não funcionará, as funções compartilhadas do helper não precisam de @login_required.
samtregar
Sim, é coxo. Quase o mais idiota que eu poderia imaginar. Você não sabe quais defs são visualizações, exceto examinando o urls.py.
S.Lott
0

Seria possível ter um único ponto de partida para todos os urlsem uma espécie de include e que o decorasse usando este pacote https://github.com/vorujack/decorate_url .

rootart
fonte
0

Existe um aplicativo que fornece uma solução plug-and-play para isso:

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

pip install django-stronghold
# settings.py

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

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