[강의 공부] 16. @login_required를 이용한 데코레이터 테스트

단간단간·2024년 5월 8일
post-thumbnail

⎷ 함수에 데코레이터 적용하기

views.py > hello_world 원래 함수

로그인하지 않은 사용자가 접근할 때, 로그인 페이지로 리다이렉트함

# views.py

def hello_world(request):
    if request.user.is_authenticated:
        if request.method == "POST":
            text = request.POST.get("hello_world_input")

            # DB 저장
            new_hello_world = HelloWorld()
            new_hello_world.text = text
            new_hello_world.save()

            return HttpResponseRedirect(reverse('accountapp:hello_world'))
        else:
            hello_world_list = HelloWorld.objects.all()
            return render(request, 'accountapp/hello_world.html', context={'hello_world_list': hello_world_list})
    else:
        return HttpResponseRedirect(reverse('accountapp:login'))

@login_required 데코레이터 사용

login_required 데코레이터 특징

  • 로그인한 사용자만 접근할 수 있게 함
  • 로그인하지 않은 사용자가 접근하면 자동으로 로그인 페이지로 리다이렉션됨
from django.contrib.auth.decorators import login_required


@login_required
def hello_world(request):
	# request.user.is_authenticated 비교 로직 제거함
    if request.method == "POST":
        text = request.POST.get("hello_world_input")

        # DB 저장
        new_hello_world = HelloWorld()
        new_hello_world.text = text
        new_hello_world.save()

        return HttpResponseRedirect(reverse('accountapp:hello_world'))
    else:
        hello_world_list = HelloWorld.objects.all()
        return render(request, 'accountapp/hello_world.html', context={'hello_world_list': hello_world_list})

로그인하지 않은 사용자가 접근했을 때, 로그인 페이지가 아닌 "다른 페이지"로 리다이렉션 하고 싶다면?

1) 간단한 테스트 페이지 만들기

# view.py

def test(request):
    return render(request, 'accountapp/test.html')
<!-- template -->

{% extends "base.html" %}

{% block content %}
    <div style="text-align: center;">
        <h1>TEST PAGE</h1>
    </div>
{% endblock %}
# urls.py

from django.urls import path

from accountapp.views import hello_world, test

app_name = "accountapp"

urlpatterns = [
    path('hello_world/', hello_world, name='hello_world'),
    path('test/', test, name='test'),
]


2) 로그인페이지가 아닌 다른 페이지(위의 테스트 페이지)로 리다이렉트 시키기

  • views.py > @login_required 데코레이터 사용
  • settigns.py > login_url 설정
# views.py

from django.contrib.auth.decorators import login_required


@login_required
def hello_world(request):
    if request.method == "POST":
        text = request.POST.get("hello_world_input")

        # DB 저장
        new_hello_world = HelloWorld()
        new_hello_world.text = text
        new_hello_world.save()

        return HttpResponseRedirect(reverse('accountapp:hello_world'))
    else:
        hello_world_list = HelloWorld.objects.all()
        return render(request, 'accountapp/hello_world.html', context={'hello_world_list': hello_world_list})
# settings.py

LOGIN_URL = reverse_lazy('accountapp:test')

3) 테스트
로그인 되지 않은 상태에서 /accounts/hello_world url로 접근하면 /accounts/test url로 리다이렉트 됨


⎷ 참고 (reverse, reverse_lazy 함수)

어째서 settigns.py 에서는 장고의 reverse를 사용하지 않고 reverse_lazy를 사용해야 하는가?

reverse, reverse_lazy 공통점
장고의 reverse, reverse_lazy 함수는 URL 이름을 바탕으로 URL을 동적으로 생성해주는 역할을 한다.


reverse, reverse_lazy 차이점 (호출시점 반환방식에 차이 O)
(1) reverse:
호출 시 바로 URL을 반환해야 한다. 따라서 reverse를 사용하기 위해서는 호출 시점에 URLConf가 완전히 로드되어 있어야 한다.
(2) reverse_lazy:
URL을 즉시 생성하지 않고, 필요할 때까지 생성을 지연한다.

settings.py에서 reverse_lazy를 사용하는 이유는 django의 로딩순서 때문이다. settigns.py는 프로젝트의 구성 설정을 포함하며, 프로젝트가 실행될 때 가장 먼저 로드된다. 때문에 설정파일이 완전히 로드되기 전까지는 URLConf가 완전히 로드되지 않을 수 있다. reverse함수의 경우 URLConf가 완전히 로드가 되어야만 정상적으로 사용할 수 있기 때문에 settigns.py에서는 사용하지 않는다.


⎷ 클래스에 데코레이터 적용하기

각 클래스뷰 기능

  • AccountUpdateView: 회원 정보 계정 수정
  • AccountDeleteView: 회원 계정 삭제

상황

  • 회원 정보 계정을 수정하고, 계정을 삭제할 때 로그인한 상태로 접근을 했는지 여부와 로그인한 유저와 접근하려는 대상의 정보가 일치하는지 여부를 확인한다. (나는 내 정보만 수정하거나 삭제가 가능해야 한다. 남의 정보를 조회할 수 없도록 해야 함)
  • 따라서 각 클래스뷰에 대해서 get, post 함수가 아래와 같이 확장되어 있는 상태
  • 코드 중복이 발생하여 이를 데코레이터로 수정할 예정
# views.py

class AccountUpdateView(UpdateView):
    model = User
    context_object_name = 'target_user'
    form_class = AccountUpdateForm
    success_url = reverse_lazy('accountapp:hello_world')
    template_name = 'accountapp/update.html'

    def get(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().get(*args, **kwargs)
        else:
            return HttpResponseForbidden("허용되지 않는 접근입니다.")

    def post(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().post(*args, **kwargs)
        else:
            return HttpResponseForbidden("허용되지 않는 접근입니다.")


class AccountDeleteView(DeleteView):
    model = User
    context_object_name = 'target_user'
    success_url = reverse_lazy('accountapp:login')
    template_name = 'accountapp/delete.html'

    def get(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().get(*args, **kwargs)
        else:
            return HttpResponseForbidden("허용되지 않는 접근입니다.")

    def post(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().post(*args, **kwargs)
        else:
            return HttpResponseForbidden("허용되지 않는 접근입니다.")

클래스에 데코레이터 적용하기

  • 데코레이터 함수 역할
    • login_required: 장고에서 지원하는 함수로, 로그인한 상태로 접근하고 있는지 확인함
    • account_ownership_required: 로그인한 유저와 접근하려는 대상이 동일한지 확인
  • 클래스 메서드에는 직접적으로 데코레이터를 사용할 수가 없다. 대신 method_decorator를 이용하면 클래스 메서드에 데코레이터를 적용시킬 수 있다.
  • @method_decorator(데코레이터함수, '클래스메서드')
# decorator.py

from django.contrib.auth.models import User
from django.http import HttpResponseForbidden


# 로그인한 유저와 접근하려는 대상의 정보가 일치하는지 여부 확인
def account_ownership_required(func):
    def wrapper(request, *args, **kwargs):
        user = User.objects.get(pk=kwargs["pk"])

        if request.user == user:
            return func(request,*args, **kwargs)
        else:
            return HttpResponseForbidden("허용되지 않는 접근입니다.")

    return wrapper
# views.py 

from django.contrib.auth.decorators import login_required


@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
@method_decorator(account_ownership_required, 'get')
@method_decorator(account_ownership_required, 'post')
class AccountUpdateView(UpdateView):
    model = User
    context_object_name = 'target_user'
    form_class = AccountUpdateForm
    success_url = reverse_lazy('accountapp:hello_world')
    template_name = 'accountapp/update.html'


@method_decorator(login_required, 'get')
@method_decorator(login_required, 'post')
@method_decorator(account_ownership_required, 'get')
@method_decorator(account_ownership_required, 'post')
class AccountDeleteView(DeleteView):
    model = User
    context_object_name = 'target_user'
    success_url = reverse_lazy('accountapp:login')
    template_name = 'accountapp/delete.html'

좀 더 간결하게 만들기

  • 두 클래스 뷰는 공통적으로 login_required, account_ownership_required 데코레이터를 사용한다.
  • 아래 has_ownership 변수와 같이 데코레이터 함수를 리스트에 담아서 사용하면 보다 간결하게 사용할 수 있다.
# views.py 

from django.contrib.auth.decorators import login_required

# login_required 먼저 순차적으로 실행됨
has_ownership = [login_required, account_ownership_required]


@method_decorator(has_ownership, 'get')
@method_decorator(has_ownership, 'post')
class AccountUpdateView(UpdateView):
    model = User
    context_object_name = 'target_user'
    form_class = AccountUpdateForm
    success_url = reverse_lazy('accountapp:hello_world')
    template_name = 'accountapp/update.html'


@method_decorator(has_ownership, 'get')
@method_decorator(has_ownership, 'post')
class AccountDeleteView(DeleteView):
    model = User
    context_object_name = 'target_user'
    success_url = reverse_lazy('accountapp:login')
    template_name = 'accountapp/delete.html'
profile
simple is best

0개의 댓글