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がありますが、これはどちらとも異なります。
Peter Rowell

回答:


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規則に従っている限り、このモデルは機能します。これが1対1で適合しない場合は、ミドルウェアを状況に合わせて変更することを選択できます。

このアプローチについて私が気に入っているの@login_requiredは、デコレータを使用してコードベースを散らかす必要性をなくすこと以外に、認証スキームが変更された場合、グローバルな変更を行うための1つの場所があるということです。


ありがとうございます。ミドルウェアで実際にlogin_required()を使用することはありませんでした。これは、ミドルウェアスタックでうまく遊んでいた問題を回避するのに役立つと思います。
samtregar、2010年

どー!これは、HTTPSである必要のあるページのグループに使用したパターンとほぼ同じであり、その他すべてはHTTPSであってはなりませ。それは2。5年前で、私はそれについて完全に忘れていました。ありがとう、ダニエル!
Peter Rowell、2010年

4
ミドルウェアRequireLoginMiddlewareクラスはどこに配置する必要がありますか?views.py、models.py?
Yasin 2013

1
@richardデコレーターはコンパイル時に実行されます。この場合、私が行ったのはfunction.public = Trueのみです。次に、ミドルウェアが実行されると、関数の.publicフラグを検索して、アクセスを許可するかどうかを決定できます。それが意味をなさない場合は、完全なコードを送信できます。
samtregar、2015年

1
最善のアプローチは、ビューに属性@publicを設定するデコレータを作成し_public、ミドルウェアがそれらのビューをスキップすることです。Djangoのcsrf_exemptデコレータは同じように機能します
Ivan Virabyan

31

各ビュー関数にデコレータを配置する代わりの方法があります。ファイルにlogin_required()デコレータを置くこともできurls.pyます。これはまだ手動のタスクですが、少なくとも1か所にすべてあるため、監査が容易になります。

例えば、

    from my_views import home_view

    urlpatterns = patterns( ''、
        # "ホーム":
        (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 2018

@TianLoon私の更新された答えを見てください、それが役立つかもしれません。
andyandy 2018

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適切なデコレータなしでs の出力を調べます。


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.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 '20 / 04/18

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年

また、classes-as-classesのすてきなアイデア...しかし、この時点でアプリの何百ものビューを書き換えることは、おそらく初心者ではないと思います。
samtregar、2010年

@samtregar:あなたは勝たなければなりませんか?私は新しいベントレーを持っている必要があります。真剣に。をgrepできdefます。非常に短いスクリプトを簡単に作成してdef、すべてのビューモジュールのすべてをスキャンし、@ login_requiredを忘れたかどうかを判断できます。
S.Lott、2010年

8
@ S.Lottこれはこれを行うための最も可能な方法ですが、そうです、うまくいくと思います。どのdefがビューであるかをどうやって知るのですか?views.pyで関数を見るだけでは機能しません。ヘルパー共有関数では@login_requiredは必要ありません。
samtregar、2010年

はい、それは不自由です。私が考えることができるほとんど最も薄っぺらい。どのdefがビューであるかは、を調べない限りわかりませんurls.py
S.Lott、2010年


0

これに対するプラグアンドプレイソリューションを提供するアプリがあります。

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

pip install django-stronghold
# settings.py

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

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.