วิธีที่ดีที่สุดในการตั้งค่า login_required ของ Django เป็นค่าเริ่มต้น


103

ฉันกำลังทำงานกับแอป Django ขนาดใหญ่ซึ่งส่วนใหญ่ต้องใช้การเข้าสู่ระบบเพื่อเข้าถึง ซึ่งหมายความว่าทั้งหมดในแอปของเราเราได้โรย:

@login_required
def view(...):

ไม่เป็นไรและใช้งานได้ดีตราบใดที่เราอย่าลืมเพิ่มเข้าไปทุกที่ ! น่าเศร้าที่บางครั้งเราลืมไปและความล้มเหลวมักไม่ปรากฏให้เห็นอย่างชัดเจน หากลิงก์เดียวไปยังข้อมูลพร็อพเพอร์ตี้อยู่ในหน้า @login_required คุณจะไม่สังเกตเห็นว่าคุณสามารถเข้าถึงมุมมองนั้นได้โดยไม่ต้องเข้าสู่ระบบ แต่ผู้ไม่หวังดีอาจสังเกตเห็นซึ่งเป็นปัญหา

ความคิดของฉันคือการย้อนกลับระบบ แทนที่จะต้องพิมพ์ @login_required ทุกที่ แต่ฉันต้องการ:

@public
def public_view(...):

เพียงเพื่อสิ่งสาธารณะ ฉันพยายามใช้สิ่งนี้กับมิดเดิลแวร์บางตัวและดูเหมือนจะไม่สามารถใช้งานได้ ทุกสิ่งที่ฉันพยายามโต้ตอบกับมิดเดิลแวร์อื่น ๆ ที่เราใช้ไม่ดีฉันคิดว่า ต่อไปฉันลองเขียนบางอย่างเพื่อสำรวจรูปแบบ URL เพื่อตรวจสอบว่าทุกสิ่งที่ไม่ใช่ @ สาธารณะถูกทำเครื่องหมายเป็น @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 สำหรับหน้าที่ต้องมีการตรวจสอบสิทธิ์โมเดลนี้ก็จะใช้งานได้ หากนี่ไม่ใช่แบบหนึ่งต่อหนึ่งคุณอาจเลือกที่จะปรับเปลี่ยนมิดเดิลแวร์เพื่อให้เหมาะกับสถานการณ์ของคุณอย่างใกล้ชิดยิ่งขึ้น

สิ่งที่ฉันชอบเกี่ยวกับแนวทางนี้ - นอกเหนือจากการกำจัดความจำเป็นในการทิ้งโค้ดเบสด้วย@login_requiredมัณฑนากร - ก็คือหากรูปแบบการรับรองความถูกต้องเปลี่ยนไปคุณจะมีที่เดียวเพื่อทำการเปลี่ยนแปลงทั่วโลก


ขอบคุณดูดีมาก! มันไม่ได้เกิดขึ้นกับฉันที่จะใช้ login_required () ในมิดเดิลแวร์ของฉัน ฉันคิดว่าสิ่งนี้จะช่วยแก้ไขปัญหาที่ฉันเล่นได้ดีกับสแต็กมิดเดิลแวร์ของเรา
samtregar

ดู๊! นี่เป็นรูปแบบที่เราใช้กับกลุ่มเพจที่ต้องเป็น HTTPS และอย่างอื่นต้องไม่ใช่ HTTPS นั่นคือ 2.5 ปีที่แล้วและฉันลืมเรื่องนี้ไปแล้ว Thanx แดเนียล!
Peter Rowell

4
มิดเดิลแวร์คลาส RequireLoginMiddleware ควรวางไว้ที่ใด views.py, models.py?
ยาซีน

1
@richard มัณฑนากรทำงานในเวลาคอมไพล์และในกรณีนี้สิ่งที่ฉันทำคือ function.public = True จากนั้นเมื่อมิดเดิลแวร์รันสามารถมองหาแฟล็ก. public บนฟังก์ชันเพื่อตัดสินใจว่าจะอนุญาตให้เข้าถึงหรือไม่ หากไม่สมเหตุสมผลฉันสามารถส่งรหัสทั้งหมดให้คุณได้
samtregar

1
ฉันคิดว่าแนวทางที่ดีที่สุดคือการสร้าง@publicมัณฑนากรซึ่งกำหนด_publicแอตทริบิวต์ในมุมมองจากนั้นมิดเดิลแวร์จะข้ามมุมมองเหล่านั้นไป มัณฑนากรcsrf_exemptของ Django ทำงานในลักษณะเดียวกัน
Ivan Virabyan

31

มีทางเลือกอื่นในการวางมัณฑนากรในแต่ละฟังก์ชันมุมมอง คุณยังสามารถใส่login_required()มัณฑนากรในurls.pyไฟล์ แม้ว่าจะยังคงเป็นงานที่ต้องทำด้วยตนเอง แต่อย่างน้อยคุณก็มีทุกอย่างในที่เดียวซึ่งช่วยให้ตรวจสอบได้ง่ายขึ้น

เช่น,

    จาก my_views นำเข้า home_view

    urlpatterns = รูปแบบ ('',
        # "บ้าน":
        (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'

UPDATE: ฉันพบว่าสิ่งต่อไปนี้ใช้งานได้:

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

@TianLoon โปรดดูคำตอบที่อัปเดตของฉันมันอาจช่วยได้
andyandy

2

เป็นการยากที่จะเปลี่ยนสมมติฐานในตัวใน Django โดยไม่ต้องทำซ้ำวิธีที่ส่งมอบ url เพื่อดูฟังก์ชัน

แทนที่จะยุ่งเกี่ยวกับภายใน 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

เรียกใช้สิ่งนี้และตรวจสอบเอาต์พุตสำหรับdefs โดยไม่มีมัณฑนากรที่เหมาะสม


2

นี่คือโซลูชันมิดเดิลแวร์สำหรับ django 1.10+

middlewares ในจะต้องมีการเขียนในรูปแบบใหม่ใน 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. การสอน Django Middlewareโดย Max Goodridge

  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

แล้วคุณทำให้ฟังก์ชั่นมุมมองของคุณ subclasses LoginViewFunctionของ

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

my_real_view = MyRealView()  

ไม่บันทึกบรรทัดใด ๆ ของรหัส และมันไม่ได้ช่วยปัญหา "เราลืม" สิ่งที่คุณทำได้คือตรวจสอบโค้ดเพื่อให้แน่ใจว่าฟังก์ชั่นมุมมองเป็นวัตถุ ของชั้นที่เหมาะสม.

แต่ถึงอย่างนั้นคุณจะไม่มีทางรู้เลยว่าฟังก์ชันทุกมุมมองนั้นถูกต้องหากไม่มีชุดทดสอบหน่วย


5
ฉันไม่สามารถชนะ? แต่ฉันต้องชนะ! การแพ้ไม่ใช่ทางเลือก! แต่อย่างจริงจังฉันไม่ได้พยายามหลีกเลี่ยงการประกาศข้อกำหนดการรับรองความถูกต้อง ฉันแค่ต้องการย้อนกลับสิ่งที่ต้องประกาศ แทนที่จะต้องประกาศมุมมองส่วนตัวทั้งหมดและไม่พูดอะไรเกี่ยวกับมุมมองสาธารณะฉันต้องการประกาศมุมมองสาธารณะทั้งหมดและให้ค่าเริ่มต้นเป็นส่วนตัว
samtregar

นอกจากนี้แนวคิดที่เป็นระเบียบสำหรับการดูเป็นชั้นเรียน ... แต่ฉันคิดว่าการเขียนมุมมองหลายร้อยครั้งในแอปของฉัน ณ จุดนี้อาจไม่ใช่การเริ่มต้น
samtregar

@samtregar: คุณต้องชนะ? ฉันต้องมีเบนท์ลีย์ใหม่ อย่างจริงจัง. คุณสามารถ grep สำหรับdef's คุณสามารถเขียนสคริปต์สั้น ๆ เพื่อสแกนทั้งหมดdefในโมดูลมุมมองทั้งหมดและตรวจสอบว่า @login_required ถูกลืมหรือไม่
ล็อตต์

8
@ S.Lott นั่นเป็นวิธีที่อ่อนแอที่สุดในการทำเช่นนี้ แต่ใช่ฉันคิดว่ามันจะได้ผล ยกเว้นคุณจะรู้ได้อย่างไรว่า defs ใดเป็นมุมมอง? เพียงแค่ดูที่ฟังก์ชันใน views.py จะไม่ทำงานฟังก์ชันที่ใช้ร่วมกันของผู้ช่วยไม่จำเป็นต้องมี @login_required
samtregar

ใช่มันเป็นง่อย เกือบจะแย่ที่สุดที่ฉันคิดได้ คุณไม่รู้ว่าคำจำกัดความใดเป็นมุมมองยกเว้นโดยการตรวจสอบไฟล์urls.py.
ล็อตต์

0

จะเป็นไปได้ที่จะมีจุดเดียวเริ่มต้นสำหรับการทั้งหมดurlsในการเรียงลำดับของการรวมและตกแต่งว่ามันใช้แพคเกจนี้https://github.com/vorujack/decorate_url


โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.