Cách tốt nhất để đặt login_required của Django làm mặc định


103

Tôi đang làm việc trên một ứng dụng Django lớn, phần lớn trong số đó yêu cầu đăng nhập để truy cập. Điều này có nghĩa là tất cả các ứng dụng của chúng tôi, chúng tôi đã rắc:

@login_required
def view(...):

Điều đó tốt, và nó hoạt động tốt miễn là chúng ta nhớ thêm nó ở mọi nơi ! Đáng buồn thay, đôi khi chúng ta quên, và thất bại thường không quá rõ ràng. Nếu liên kết duy nhất đến một chế độ xem nằm trên trang @login_required thì bạn sẽ không nhận thấy rằng bạn thực sự có thể tiếp cận chế độ xem đó mà không cần đăng nhập. Nhưng kẻ xấu có thể nhận thấy, đó là một vấn đề.

Ý tưởng của tôi là đảo ngược hệ thống. Thay vì phải nhập @login_required ở khắp mọi nơi, thay vào đó tôi sẽ có một cái gì đó như:

@public
def public_view(...):

Chỉ dành cho những thứ công cộng. Tôi đã cố gắng thực hiện điều này với một số phần mềm trung gian và dường như tôi không thể làm cho nó hoạt động. Mọi thứ tôi đã thử đều tương tác không tốt với phần mềm trung gian khác mà chúng tôi đang sử dụng, tôi nghĩ vậy. Tiếp theo, tôi đã thử viết một cái gì đó để duyệt qua các mẫu URL để kiểm tra xem mọi thứ không phải là @public đã được đánh dấu @login_required hay chưa - ít nhất sau đó chúng tôi sẽ gặp lỗi nhanh nếu chúng tôi quên điều gì đó. Nhưng sau đó tôi không thể tìm ra cách để biết liệu @login_required đã được áp dụng cho một chế độ xem hay chưa ...

Vì vậy, cách thích hợp để làm điều này là gì? Cảm ơn đã giúp đỡ!


2
Câu hỏi hay. Tôi đã ở đúng vị trí cũ. Chúng tôi có phần mềm trung gian để làm cho toàn bộ trang web login_required và chúng tôi có ACL tự phát triển để hiển thị các chế độ xem / mẫu-phân đoạn khác nhau cho những người / vai trò khác nhau, nhưng điều này khác với một trong hai.
Peter Rowell

Câu trả lời:


99

Phần mềm trung gian có thể là đặt cược tốt nhất của bạn. Tôi đã sử dụng đoạn mã này trước đây, được sửa đổi từ đoạn mã được tìm thấy ở nơi khác:

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

Sau đó, trong settings.py, liệt kê các URL cơ sở bạn muốn bảo vệ:

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

Miễn là trang web của bạn tuân theo các quy ước URL cho các trang yêu cầu xác thực, mô hình này sẽ hoạt động. Nếu đây không phải là sự phù hợp 1-1, bạn có thể chọn sửa đổi phần mềm trung gian để phù hợp hơn với hoàn cảnh của mình.

Điều tôi thích ở cách tiếp cận này - bên cạnh việc loại bỏ sự cần thiết của việc rải rác cơ sở mã với trình @login_requiredtrang trí - là nếu sơ đồ xác thực thay đổi, bạn có một nơi để thực hiện các thay đổi toàn cầu.


Cảm ơn, cái này trông rất tuyệt! Tôi không thực sự sử dụng login_required () trong phần mềm trung gian của mình. Tôi nghĩ điều này sẽ giúp giải quyết vấn đề mà tôi đang gặp phải với ngăn xếp phần mềm trung gian của chúng tôi.
samtregar,

Doh! Đây gần như chính xác là mẫu mà chúng tôi đã sử dụng cho một nhóm trang phải là HTTPS và mọi thứ khác không được là HTTPS. Đó là 2,5 năm trước và tôi đã hoàn toàn quên nó. Thanx, Daniel!
Peter Rowell

4
Lớp middleware RequestLoginMiddleware nên được đặt ở đâu? views.py, models.py?
Yasin

1
@richard decorator chạy tại thời điểm biên dịch và trong trường hợp này, tất cả những gì tôi làm là: function.public = True. Sau đó, khi phần mềm trung gian chạy nó có thể tìm cờ .public trên chức năng để quyết định có cho phép truy cập hay không. Nếu điều đó không hợp lý, tôi có thể gửi cho bạn mã đầy đủ.
samtregar

1
Tôi nghĩ cách tiếp cận tốt nhất là tạo @publicdecorator, thiết lập _publicthuộc tính trên chế độ xem và phần mềm trung gian sau đó bỏ qua các chế độ xem đó. Django csrf_exempt trang trí hoạt động theo cách tương tự
Ivan Virabyan

31

Có một giải pháp thay thế là đặt một trình trang trí trên mỗi chức năng xem. Bạn cũng có thể đặt trình login_required()trang trí vào urls.pytệp. Mặc dù đây vẫn là một công việc thủ công, nhưng ít nhất bạn có tất cả ở một nơi, điều này giúp kiểm tra dễ dàng hơn.

ví dụ,

    from my_views import home_view

    urlpatterns = pattern ('',
        # "Trang Chủ":
        (r '^ $', login_required (home_view), dict (template_name = 'my_site / home.html', items_per_page = 20)),
    )

Lưu ý rằng các hàm dạng xem được đặt tên và nhập trực tiếp, không phải dưới dạng chuỗi.

Cũng lưu ý rằng điều này hoạt động với bất kỳ đối tượng dạng xem có thể gọi nào, bao gồm cả các lớp.


3

Trong Django 2.1, chúng ta có thể trang trí tất cả các phương thức trong một lớp với:

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'

CẬP NHẬT: Tôi cũng đã thấy những thứ sau hoạt động:

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

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

và đặt LOGIN_URL = '/accounts/login/'trong settings.py


1
cảm ơn cho câu trả lời mới này. nhưng xin vui lòng giải thích một chút về nó, tôi không thể hiểu nó ngay cả khi tôi đọc tài liệu chính thức. cảm ơn vì sự giúp đỡ ya trước
Tian Loon

@TianLoon vui lòng xem câu trả lời cập nhật của tôi, nó có thể hữu ích.
andyandy

2

Thật khó để thay đổi các giả định có sẵn trong Django mà không làm lại cách url được sử dụng để xem các chức năng.

Thay vì nhắc nhở trong nội bộ Django, đây là một kiểm tra mà bạn có thể sử dụng. Đơn giản chỉ cần kiểm tra từng chức năng xem.

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

Chạy điều này và kiểm tra đầu ra cho defs mà không có bộ trang trí thích hợp.


2

Đây là giải pháp phần mềm trung gian cho django 1.10+

Phần mềm trung gian trong phải được viết theo cách mới trong 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

Cài đặt

  1. Sao chép mã vào thư mục dự án của bạn và lưu dưới dạng middleware.py
  2. Thêm vào MIDDLEWARE

    MIDDLEWARE = ​​[... '.middleware.RequireLoginMiddleware', # Yêu cầu đăng nhập]

  3. Thêm vào settings của bạn.py:
LOGIN_REQUIRED_URLS = (
    r'(.*)',
)
LOGIN_REQUIRED_URLS_EXCEPTIONS = (
    r'/admin(.*)$',
)
LOGIN_URL = '/admin'

Nguồn:

  1. Câu trả lời này của Daniel Naab

  2. Hướng dẫn về phần mềm trung gian Django của Max Goodridge

  3. Django Middleware Docs


Lưu ý rằng mặc dù có gì xảy ra trong __call__, các process_viewmóc vẫn được sử dụng [sửa]
Simon Kohlmeyer

1

Lấy cảm hứng từ câu trả lời của Ber, tôi đã viết một đoạn mã nhỏ thay thế patternshàm, bằng cách gói tất cả các lệnh gọi lại URL bằng trình login_requiredtrang trí. Điều này hoạt động trong 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

Sử dụng nó hoạt động như thế này (cuộc gọi đến listlà bắt buộc vì yield).

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

0

Bạn không thể thực sự giành được điều này. Bạn chỉ cần khai báo các yêu cầu ủy quyền. Bạn sẽ đặt khai báo này ở đâu khác ngoại trừ ngay bởi chức năng xem?

Xem xét thay thế các chức năng dạng xem của bạn bằng các đối tượng có thể gọi được.

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

Sau đó, bạn tạo các lớp con của hàm xem của bạn LoginViewFunction.

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

my_real_view = MyRealView()  

Nó không lưu bất kỳ dòng mã nào. Và nó không giúp ích gì cho vấn đề "chúng ta đã quên". Tất cả những gì bạn có thể làm là kiểm tra mã để đảm bảo rằng các hàm xem là đối tượng. Thuộc đẳng cấp phù hợp.

Nhưng ngay cả khi đó, bạn sẽ không bao giờ thực sự biết rằng mọi chức năng xem đều đúng nếu không có bộ thử nghiệm đơn vị.


5
Tôi không thể thắng? Nhưng tôi phải chiến thắng! Thua không phải là một lựa chọn! Nhưng nghiêm túc mà nói, tôi không cố gắng tránh khai báo các yêu cầu xác thực của mình. Tôi chỉ muốn đảo ngược những gì cần khai báo. Thay vì phải khai báo tất cả các chế độ xem riêng tư và không nói gì về các chế độ xem công khai, tôi muốn khai báo tất cả các chế độ xem công khai và đặt mặc định là riêng tư.
samtregar,

Ngoài ra, ý tưởng gọn gàng cho các chế độ xem dưới dạng lớp ... Nhưng tôi nghĩ việc viết lại hàng trăm chế độ xem trong ứng dụng của mình tại thời điểm này có lẽ là một việc không bắt đầu.
samtregar,

@samtregar: Bạn phải thắng? Tôi phải có một chiếc Bentley mới. Nghiêm túc. Bạn có thể grep for def's. Bạn có thể viết một đoạn script rất ngắn để quét tất cả deftrong tất cả các mô-đun chế độ xem và xác định xem @login_required có bị quên hay không.
S.Lott

8
@ S.Lott Đó là cách tốt nhất có thể để làm điều này, nhưng vâng, tôi đoán nó sẽ hoạt động. Ngoại trừ làm thế nào để bạn biết đó là lượt xem defs? Chỉ nhìn vào các chức năng trong views.py sẽ không hoạt động, các chức năng được chia sẻ của trình trợ giúp không cần @login_required.
samtregar

Vâng, nó khập khiễng. Gần như là điều tồi tệ nhất mà tôi có thể nghĩ đến. Bạn không biết def là lượt xem nào ngoại trừ bằng cách kiểm tra urls.py.
S.Lott


0

Có một ứng dụng cung cấp giải pháp plug-and-play cho việc này:

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

pip install django-stronghold
# settings.py

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

MIDDLEWARE_CLASSES = (
    #...
    'stronghold.middleware.LoginRequiredMiddleware',
)
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.