Cara terbaik untuk membuat login_required Django sebagai default

103

Saya sedang mengerjakan aplikasi Django yang besar, yang sebagian besar membutuhkan login untuk mengakses. Ini berarti bahwa di seluruh aplikasi kami, kami telah menaburkan:

@login_required
def view(...):

Tidak apa-apa, dan berfungsi dengan baik selama kita ingat untuk menambahkannya di mana-mana ! Sayangnya terkadang kita lupa, dan kegagalan seringkali tidak terlalu terbukti. Jika satu-satunya tautan ke tampilan ada di laman @login_required maka Anda tidak akan menyadari bahwa Anda benar-benar dapat mencapai tampilan itu tanpa masuk. Tetapi orang jahat mungkin memperhatikan, yang merupakan masalah.

Ide saya adalah membalikkan sistem. Alih-alih harus mengetik @login_required di mana-mana, saya malah memiliki sesuatu seperti:

@public
def public_view(...):

Hanya untuk urusan umum. Saya mencoba menerapkan ini dengan beberapa middleware dan saya tidak bisa membuatnya bekerja. Semua yang saya coba berinteraksi buruk dengan middleware lain yang kami gunakan, saya kira. Selanjutnya saya mencoba menulis sesuatu untuk menelusuri pola URL untuk memeriksa bahwa semua yang bukan @public telah ditandai @login_required - setidaknya kita akan mendapatkan kesalahan cepat jika kita lupa sesuatu. Tapi kemudian saya tidak tahu bagaimana cara mengetahui apakah @login_required telah diterapkan ke tampilan ...

Jadi, apa cara yang benar untuk melakukan ini? Terima kasih untuk bantuannya!

samtregar.dll
sumber
2
Pertanyaan yang bagus. Saya pernah berada di posisi yang persis sama. Kami memiliki middleware untuk membuat seluruh situs login_required, dan kami memiliki ACL buatan sendiri untuk menampilkan tampilan / template-fragmen yang berbeda ke orang / peran yang berbeda, tetapi ini berbeda dari keduanya.
Peter Rowell

Jawaban:

99

Middleware mungkin merupakan pilihan terbaik Anda. Saya telah menggunakan potongan kode ini sebelumnya, dimodifikasi dari cuplikan yang ditemukan di tempat lain:

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

Kemudian di settings.py, daftarkan URL dasar yang ingin Anda lindungi:

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

Selama situs Anda mengikuti konvensi URL untuk halaman yang memerlukan autentikasi, model ini akan berfungsi. Jika ini bukan kesesuaian one-to-one, Anda dapat memilih untuk memodifikasi middleware agar lebih sesuai dengan keadaan Anda.

Apa yang saya suka tentang pendekatan ini - selain menghilangkan kebutuhan untuk mengotori basis kode dengan @login_requireddekorator - adalah bahwa jika skema otentikasi berubah, Anda memiliki satu tempat untuk pergi untuk membuat perubahan global.

Daniel Naab
sumber
Terima kasih, ini terlihat bagus! Tidak terpikir oleh saya untuk benar-benar menggunakan login_required () di middleware saya. Saya pikir ini akan membantu mengatasi masalah yang saya alami dengan tumpukan middleware kami.
samtregar
Doh! Ini hampir persis dengan pola yang kami gunakan untuk sekelompok laman yang harus HTTPS, dan yang lainnya tidak boleh HTTPS. Itu 2,5 tahun yang lalu dan saya benar-benar melupakannya. Terima kasih, Daniel!
Peter Rowell
4
Kelas middleware RequireLoginMiddleware harus ditempatkan di mana? views.py, models.py?
Yasin
1
@richard dekorator berjalan pada waktu kompilasi, dan dalam hal ini yang saya lakukan hanyalah: function.public = True. Kemudian ketika middleware berjalan, ia dapat mencari tanda .public pada fungsi tersebut untuk memutuskan apakah akan mengizinkan akses atau tidak. Jika itu tidak masuk akal, saya dapat mengirimkan kode lengkapnya kepada Anda.
samtregar
1
Saya pikir pendekatan terbaik adalah membuat @publicdekorator, yang menyetel _publicatribut pada tampilan, dan middleware kemudian melewatkan tampilan tersebut. Dekorator csrf_exempt Django bekerja dengan cara yang sama
Ivan Virabyan
31

Ada alternatif untuk menempatkan dekorator pada setiap fungsi tampilan. Anda juga bisa meletakkan login_required()dekorator di urls.pyfile. Meskipun ini masih merupakan tugas manual, setidaknya Anda memiliki semuanya di satu tempat, yang memudahkan untuk mengaudit.

misalnya,

    dari my_views import home_view

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

Perhatikan bahwa fungsi tampilan diberi nama dan diimpor secara langsung, bukan sebagai string.

Perhatikan juga bahwa ini berfungsi dengan objek tampilan yang dapat dipanggil, termasuk kelas.

Ber
sumber
3

Dalam Django 2.1, kita dapat menghias semua metode dalam kelas dengan:

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: Saya juga menemukan yang berikut ini berfungsi:

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

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

dan setel LOGIN_URL = '/accounts/login/'di settings.py Anda

andyandy
sumber
1
terima kasih atas jawaban baru ini. tapi tolong jelaskan sedikit tentang itu, saya tidak bisa mendapatkannya bahkan jika saya membaca dokumen resmi. terima kasih atas bantuannya sebelumnya
Tian Loon
@TianLoon silakan lihat jawaban saya yang diperbarui, ini dapat membantu.
andyandy
2

Sulit untuk mengubah asumsi built-in dalam Django tanpa mengerjakan ulang cara url diserahkan untuk melihat fungsi.

Daripada menyia-nyiakan tentang internal Django, berikut ini audit yang dapat Anda gunakan. Cukup periksa setiap fungsi tampilan.

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

Jalankan ini dan periksa keluaran untuk defs tanpa dekorator yang sesuai.

S. Lott
sumber
2

Berikut adalah solusi middleware untuk django 1.10+

Middleware di harus ditulis dengan cara baru di django 1.10+ .

Kode

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

Instalasi

  1. Salin kode tersebut ke folder proyek Anda, dan simpan sebagai middleware.py
  2. Tambahkan ke MIDDLEWARE

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

  3. Tambahkan ke pengaturan Anda.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Sumber:

  1. Jawaban ini oleh Daniel Naab

  2. Tutorial Django Middleware oleh Max Goodridge

  3. Django Middleware Docs

np8
sumber
Perhatikan bahwa meskipun tidak terjadi apa-apa __call__, process_viewpengait tersebut masih digunakan [diedit]
Simon Kohlmeyer
1

Terinspirasi oleh jawaban Ber, saya menulis potongan kecil yang menggantikan patternsfungsi tersebut, dengan membungkus semua panggilan balik URL dengan login_requireddekorator. Ini bekerja di 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

Menggunakannya bekerja seperti ini (panggilan ke listdiperlukan karena yield).

urlpatterns = list(login_required_patterns('', url(r'^$', home_view)))
persegi panjang
sumber
0

Anda tidak bisa memenangkan ini. Anda hanya perlu membuat pernyataan tentang persyaratan otorisasi. Di mana lagi Anda akan meletakkan deklarasi ini kecuali right by view function?

Pertimbangkan untuk mengganti fungsi tampilan Anda dengan objek yang dapat dipanggil.

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

Anda kemudian membuat subkelas dari fungsi tampilan LoginViewFunction.

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

my_real_view = MyRealView()  

Itu tidak menyimpan baris kode apa pun. Dan itu tidak membantu masalah "kita lupa". Yang dapat Anda lakukan hanyalah memeriksa kode untuk memastikan bahwa fungsi tampilan adalah objek. Dari kelas yang tepat.

Namun meskipun demikian, Anda tidak akan pernah benar-benar tahu bahwa setiap fungsi tampilan sudah benar tanpa rangkaian pengujian unit.

S. Lott
sumber
5
Saya tidak bisa menang? Tapi saya harus menang! Kalah bukanlah pilihan! Tapi serius, saya tidak berusaha menghindari pernyataan persyaratan autentikasi saya. Saya hanya ingin membalikkan apa yang perlu diumumkan. Alih-alih harus mendeklarasikan semua pandangan pribadi dan tidak mengatakan apa-apa tentang pandangan publik, saya ingin menyatakan semua pandangan publik dan defaultnya adalah pribadi.
samtregar
Juga, ide yang bagus untuk views-as-class ... Tapi menurut saya menulis ulang ratusan view di aplikasi saya pada saat ini mungkin bukan hal yang baru.
samtregar
@ Samtregar: Anda harus menang? Saya harus punya Bentley baru. Sungguh. Anda bisa grep untuk def. Anda dapat dengan mudah menulis skrip yang sangat pendek untuk memindai semua yang ada defdi semua modul tampilan dan menentukan apakah @login_required dilupakan.
S. Lotot
8
@ S.Lott Itu adalah cara yang paling buruk untuk melakukan ini, tapi ya, saya rasa itu akan berhasil. Kecuali bagaimana Anda mengetahui definisi mana yang merupakan pandangan? Hanya melihat fungsi di views.py tidak akan berfungsi, fungsi pembantu bersama di sana tidak perlu @login_required.
samtregar
Ya, itu payah. Hampir yang paling buruk yang bisa saya pikirkan. Anda tidak tahu def mana yang merupakan tampilan kecuali dengan memeriksa urls.py.
S. Lotot
0

Mungkinkah memiliki satu titik awal untuk semua urlsdalam semacam penyertaan dan yang menghiasinya menggunakan paket ini https://github.com/vorujack/decorate_url .

rootart
sumber
0

Ada aplikasi yang menyediakan solusi plug-and-play untuk ini:

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

pip install django-stronghold
# settings.py

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

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