使Django的login_required为默认值的最佳方法


103

我正在开发一个大型Django应用程序,其中绝大多数需要登录才能访问。这意味着我们在整个应用程序中都花了很多钱:

@login_required
def view(...):

很好,只要我们记得将其添加到任何地方,它就可以很好地工作!可悲的是,有时我们忘记了,而且失败往往不是很明显。如果到视图的唯一链接是在@login_required页面上,则您不太可能注意到实际上无需登录即可进入该视图。但是,坏人可能会注意到,这是一个问题。

我的想法是反转系统。不必在任何地方键入@login_required,而是有类似以下内容:

@public
def public_view(...):

仅用于公共物品。我尝试使用一些中间件来实现它,但似乎无法使它正常工作。我认为,我尝试的所有内容都与我们正在使用的其他中间件进行了严重的交互。接下来,我尝试编写一些内容来遍历URL模式,以检查是否所有非@public都标记为@login_required-至少如果忘记了某些内容,我们将很快得到错误提示。但是后来我不知道如何判断@login_required是否已应用于视图...

那么,什么是正确的方法呢?谢谢您的帮助!


2
很好的问题。我一直处于完全相同的位置。我们具有用于使整个网站具有 login_required 要求的中间件,并且我们拥有一种自定义的ACL,用于向不同的人员/角色显示不同的视图/模板片段,但这与这两个都不相同。
彼得·罗威尔

Answers:


99

中间件可能是您最好的选择。我过去使用过这段代码,是在其他地方的代码段中进行了修改:

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

然后在settings.py中,列出您要保护的基本URL:

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

只要您的站点遵循要求身份验证的页面的URL约定,此模型就可以工作。如果这不是一对一的适合,您可以选择修改中间件以更紧密地适应您的情况。

我喜欢这种方法-除了消除了用@login_required修饰符乱填充代码库的必要性外,还在于,如果身份验证方案发生更改,那么您还有一个地方可以进行全局更改。


谢谢,这看起来很棒!我没有在中间件中实际使用login_required()。我认为这将有助于解决我在中间件堆栈上玩得很好的问题。
samtregar,2010年

h!这几乎就是我们用于一组必须为HTTPS 的页面所使用的模式,其他所有内容都不得为HTTPS。那是2.5年前,我完全忘记了它。谢谢丹尼尔!
彼得·罗威尔

4
中间件的RequireLoginMiddleware类应该放在哪里?views.py,models.py?
Yasin

1
@richard装饰器在编译时运行,在这种情况下,我所做的只是:function.public = True。然后,当中间件运行时,它可以在函数上寻找.public标志,以决定是否允许访问。如果那没有意义,我可以向您发送完整的代码。
samtregar,2015年

1
我认为最好的方法是制作@public装饰器,该装饰器_public在视图上设置属性,然后中间件跳过这些视图。Django的csrf_exempt装饰以同样的方式
伊万Virabyan

31

除了在每个视图函数上放置装饰器之外,还有另一种方法。您也可以将login_required()装饰器放入urls.py文件中。尽管这仍然是一项手动任务,但至少您将所有功能都集中在一个地方,这使得审核更加容易。

例如,

    从my_views导入home_view

    urlpatterns = pattern('',
        #“首页”:
        (r'^ $',login_required(home_view),dict(template_name ='my_site / home.html',items_per_page = 20)),
    )

请注意,视图函数是直接命名和导入的,而不是字符串。

还要注意,这适用于任何可调用的视图对象,包括类。


3

在Django 2.1中,我们可以使用以下方法装饰类中的所有方法

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'

更新: 我还发现以下工作:

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

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

LOGIN_URL = '/accounts/login/'在您的settings.py中进行设置


1
感谢您的新答案。但是请对此解释一点点,即使我阅读了官方文档也无法理解。感谢您的提前帮助
Tian Loon '18

@TianLoon请参阅我的最新答案,可能会有帮助。
andyandy '18

2

如果不修改url传递给视图函数的方式,就很难更改Django中的内置假设。

您可以使用审计来代替Django内部的混乱。只需检查每个视图功能。

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

运行此命令,并在def没有适当修饰符的情况下检查输出的。


2

这是Django 1.10+的中间件解决方案

必须使用django 1.10+中的新方式编写其中的中间件。

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

安装

  1. 将代码复制到您的项目文件夹中,并另存为middleware.py
  2. 添加到MIDDLEWARE

    MIDDLEWARE = [...'.middleware.RequireLoginMiddleware',#要求登录]

  3. 添加到您的settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

资料来源:

  1. Daniel Naab的答案

  2. Max Goodridge的Django中间件教程

  3. Django中间件文档


请注意,尽管没有任何反应,但仍使用__call__process_view钩子[编辑]
Simon Kohlmeyer

1

受Ber的回答启发,我编写了一个小片段来替换该patterns函数,方法是将所有URL回调都包装在login_required装饰器中。这在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

使用它的方式是这样的(list由于,必须调用yield)。

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

0

你不能真正赢得这场胜利。您只需必须做出的授权要求的声明。除了在视图函数中正确的位置之外,您还将把此声明放在哪里?

考虑用可调用对象替换视图函数。

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

然后,将您的视图函数设为的子类LoginViewFunction

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

my_real_view = MyRealView()  

它不保存任何代码行。而且这对解决“我们忘记了”的问题没有帮助。您所要做的就是检查代码,以确保视图函数是对象。正确的阶级。

但是即使那样,您也永远不会真正知道 没有单元测试套件每个视图功能都是正确的。


5
我赢不了?但是我必须赢!输球不是一种选择!但认真的说,我并不是想避免声明自己的身份验证要求。我只想撤销需要声明的内容。我不必声明所有私有视图并且不对公共视图说什么,而是要声明所有公共视图并将默认设置为私有。
samtregar,2010年

另外,关于按类进行视图的巧妙想法...但是我认为此时重写应用程序中的数百个视图可能不是一个开始。
samtregar,2010年

@samtregar:你必须赢吗?我必须有一个新的宾利。说真的 您可以为def的grep 。您可以编写一个简短的脚本来扫描def所有视图模块中的所有脚本,并确定是否忘记了@login_required。
S.Lott

8
@ S.Lott这是执行此操作的最可行的方法,但是是的,我想它会起作用。除了您如何知道视图是哪些定义之外?仅仅查看views.py中的函数是行不通的,那里的helper共享函数不需要@login_required。
samtregar,2010年

是的,很la脚。我想到的几乎是最沉闷的。除了检查之外,您不知道哪些定义是视图urls.py
S.Lott


0

有一个应用程序为此提供了即插即用的解决方案:

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

pip install django-stronghold
# settings.py

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

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.