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

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

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

@login_required
def view(...):

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

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

@public
def public_view(...):

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

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

I’m working on a large Django app, the vast majority of which requires a login to access. This means that all throughout our app we’ve sprinkled:

@login_required
def view(...):

That’s fine, and it works great as long as we remember to add it everywhere! Sadly sometimes we forget, and the failure often isn’t terribly evident. If the only link to a view is on a @login_required page then you’re not likely to notice that you can actually reach that view without logging in. But the bad guys might notice, which is a problem.

My idea was to reverse the system. Instead of having to type @login_required everywhere, instead I’d have something like:

@public
def public_view(...):

Just for the public stuff. I tried to implement this with some middleware and I couldn’t seem to get it to work. Everything I tried interacted badly with other middleware we’re using, I think. Next up I tried writing something to traverse the URL patterns to check that everything that’s not @public was marked @login_required – at least then we’d get a quick error if we forgot something. But then I couldn’t figure out how to tell if @login_required had been applied to a view…

So, what’s the right way to do this? Thanks for the help!


回答 0

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

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修饰符乱填充代码库的必要性外,还在于,如果身份验证方案发生更改,那么您还有一个地方可以进行全局更改。

Middleware may be your best bet. I’ve used this piece of code in the past, modified from a snippet found elsewhere:

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

Then in settings.py, list the base URLs you want to protect:

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

As long as your site follows URL conventions for the pages requiring authentication, this model will work. If this isn’t a one-to-one fit, you may choose to modify the middleware to suit your circumstances more closely.

What I like about this approach – besides removing the necessity of littering the codebase with @login_required decorators – is that if the authentication scheme changes, you have one place to go to make global changes.


回答 1

除了在每个视图函数上放置装饰器之外,还有另一种方法。您也可以将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)),
    )

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

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

There is an alternative to putting a decorator on each view function. You can also put the login_required() decorator in the urls.py file. While this is still a manual task, at least you have it all in one place, which makes it easier to audit.

e.g.,

    from my_views import home_view

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

Note that view functions are named and imported directly, not as strings.

Also note that this works with any callable view object, including classes.


回答 2

在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中进行设置

In Django 2.1, we can decorate all methods in a class with:

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'

UPDATE: I have also found the following to work:

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

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

and set LOGIN_URL = '/accounts/login/' in your settings.py


回答 3

如果不修改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没有适当修饰符的情况下检查输出的。

It’s hard to change the built-in assumptions in Django without reworking the way url’s are handed off to view functions.

Instead of mucking about in Django internals, here’s an audit you can use. Simply check each view function.

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

Run this and examine the output for defs without appropriate decorators.


回答 4

这是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中间件文档

Here is a middleware solution for django 1.10+

The middlewares in have to be written in a new way in 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. Copy the code into your project folder, and save as middleware.py
  2. Add to MIDDLEWARE

    MIDDLEWARE = [ … ‘.middleware.RequireLoginMiddleware’, # Require login ]

  3. Add to your settings.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Sources:

  1. This answer by Daniel Naab

  2. Django Middleware tutorial by Max Goodridge

  3. Django Middleware Docs


回答 5

受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)))

Inspired by Ber’s answer I wrote a little snippet that replaces the patterns function, by wrapping all of the URL callbacks with the login_required decorator. This works 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

Using it works like this (the call to list is required because of the yield).

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

回答 6

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

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

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()  

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

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

You can’t really win this. You simply must make a declaration of the authorization requirements. Where else would you put this declaration except right by the view function?

Consider replacing your view functions with callable objects.

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

You then make your view functions subclasses of LoginViewFunction.

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

my_real_view = MyRealView()  

It doesn’t save any lines of code. And it doesn’t help the “we forgot” problem. All you can do is examine the code to be sure that the view functions are objects. Of the right class.

But even then, you’ll never really know that every view function is correct without a unit test suite.


回答 7

可能会为所有包含一个单一起点urls,并使用此包https://github.com/vorujack/decorate_url装饰它。

Would be possible to have a single starting point for all the urls in a sort of include and that decorate it using this packages https://github.com/vorujack/decorate_url.


回答 8

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

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

pip install django-stronghold
# settings.py

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

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

There’s an app that provides a plug-and-play solution to this:

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

pip install django-stronghold
# settings.py

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

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