출처: https://meyouus.tistory.com/64 [정보 공유 - For Me For You For Us]
본문으로 바로가기

www.youtube.com/watch?v=eBsc65jTKvw&list=PL-51WBLyFTg2vW-_6XBoUpE7vpmoR3ztO&index=15

 

이 포스팅은 다음과 같은 youtube 영상을 따라하면서 배운 내용과 학습 내용을 담은 것이다:

  • Dennis Ivy - User Role Based Permissions & Authentications | Django Framework (3.0) Crash Course Tutorials (pt 14)

현재 Django Version 3.1.2 를 쓰고 있다. Django는 파이썬을 쓰는 오픈소스 웹 프레임워크이다. 웹사이트 만들때 주로 쓰며, 간단한 것이 큰 특징이다.

 

 

※ Decorator를 만들어 회원가입/로그인 페이지 접근 관리하기

 

일단 우리가 저번에 views.py에서 구현한 registerlogin 함수를 한번 보자:

# views.py
...
def registerPage(request):
	if requests.user.is_authenticated:
		return redirect('home')
	else:
		form = CreateUserForm()
		if request.method == 'POST':
		form = CreateUserForm(request.POST)
			if form.is_valid():
				form.save()
				return redirect('login')
		context = {'form':form}
		return render(request, 'accounts/register.html', context)

def loginPage(request):
	if requests.user.is_authenticated:
		return redirect('home')
	else:
      if request.method == 'POST':
          username = request.POST.get('username')
          password = request.POST.get('password')
          user = authenticate(request, username=username, password=password)
          if user is not None:
              login(request, user)
              return redirect ('home')
          else:
              messages.info(request, 'Username OR Password is Incorrect')
      context = {}
      return render(request, 'accounts/login.html', context)
...

여기서 우리는 registerPageloginPage에서 request.user.is_authenticated를 이용하여, 로그인된 사용자들은 이 페이지들에 접근하지 못하고, 바로 홈페이지로 돌아가게끔 설정해놨다.

 

하지만 여기서의 문제는 만약 이러한 페이지들이 한 두개가 아닌 수십개가 된다면 일일이 해줘야 하기 때문에 가독성면에서도, 효율성 면에서도 떨어진다는 것이다. 따라서 이를 하기 위해서는 custom decorator를 만들어 해결해주면 된다:

 

우선, decorators.py를 만들어준다: 

crm1
ㄴ accounts
   ㄴtemplates
     ㄴaccounts
   ㄴ __pycyache__
   ㄴ migrations
   ㄴ __init__.py
   ㄴ admin.py
   ㄴ apps.py
   ㄴ models.py
   ㄴ tests.py
   ㄴ views.py
   ㄴ forms.py
   ㄴ filters.py
   ㄴ decorators.py
ㄴ cmr1
ㄴstatic
ㄴ db.sqlite3
ㄴ manage.py

이제 이 decorators.py안에 다음과 같이 입력한다:

# decorators.py
from django.http import HttpResponse
from django.shortcuts import redirect

def unauthenticated_user(view_func):
	def wrapper_func(request, *args, **kwargs):
		if request.user.is_authenticated:
			return redirect('home')
		else:
			return view_func(request, *args, **kwargs)
		return view_func (request, *args, **kwargs)
	return wrapper_func

차근차근 설명해보도록 하겠다.

 

여기서 unauthenticated_user은, 우리가 decorator로서 사용하게 될 decorator의 이름이 되고, (view_func)는 우리가 views.py에 있는 그 특정 function을 불러올 것이라는 것을 의미한다. (이에 대해서는 밑에 조금 더 자세히 설명해놨다)

wrapper_func의 경우, decorator의 본 함수 안에 있는 함수라고 해서 wrapper_func이라고 이름을 붙인것이라고 생각이 되는데, 여기에는 request, *args, **kwargs를 전달해준다. (*args**kwargs에 대해서는 자세히 다룬 블로그가 많으므로 참고하길 바란다. 이에 대해서 나도 한번 다루어볼 예정이다.) 이 wrapper_func이 먼저 실행되고, 이 실행된 값은 unauthenticated_user이라는 함수가 통과시켜주는 것으로 decorator이 완성된다.

 

따라서 여기서 if문을 통해 만약 사용자가 authenticated 이라면 바로 홈페이지로 돌아가게끔 하고, 아니라면 view_func(request, *args, **kwargs)를 통해 우리가 사용한 views.py의 함수를 불러일으켜준다. 이후, 이 wrapper_func을 돌려줌으로써 이 decorator 함수는 끝나게 된다.

 

이를 사용하기 위해서는 저번의 @login_required(login_url='login)와 비슷하게 해주면 된다:

# views.py
...
@unauthenticated_user
def registerPage(request):
	form = CreateUserForm()
	if request.method == 'POST':
		form = CreateUserForm(request.POST)
		if form.is_valid():
			form.save()
			return redirect('login')
	context = {'form':form}
	return render(request, 'accounts/register.html', context)

@unauthenticated_user
def loginPage(request):
	if request.method == 'POST':
		username = request.POST.get('username')
		password = request.POST.get('password')
		user = authenticate(request, username=username, password=password)
		if user is not None:
			login(request, user)
			return redirect ('home')
		else:
			messages.info(request, 'Username OR Password is Incorrect')
	context = {}
	return render(request, 'accounts/login.html', context)
...

함수 위에 붙여주면 되고, 앞서 우리가 구현했던 if request.user.is_authenticated는 쓰지 않아도 된다.

 

 

※ Decorator를 만들어 특정 그룹의 사용자만 특정 행위를 허용하기(1):

 

이번에 해결할 문제는, 우리가 지금까지 부르던 product.html, customer.html, delete.html 등등은 사실 특정 사용자만(사이트 관리자들) 접근이 가능해야 했다. 하지만 실제로는 아무나 들어갈 수 있었다. 이에 대한 대책은 똑같이 decorator를 사용하는 것이다. 이번에 decorators.py에 다음과 같이 추가한다:

# decorators.py
...
def allowed_users(allowed_roles=[]):
	def decorator(view_func):
		def wrapper_func(request, *args, **kwargs):
			group = None
			if request.user.groups.exists():
				group = request.user.groups.all()[0].name
			if group in allowed_roles:
				return view_func(request, *args, **kwargs)
			else:
				return HttpResponse('You are not authorized to see this page')
			return view_func(request, *args, **kwargs)
		return wrapper_func
	return decorator

방금 우리가 구현한 unauthenticated_user과는 사뭇 다른 모습이다. 이유는 unauthenticator_user의 경우, 우리가 원하는 방식이 딱 한 방식이다. 즉, 허용됐을 경우 A를 하고, 아니라면 B를 하고.

하지만 이번에 우리가 구현하는 내용은 "A일때 허용됐으면 AA를 하고, 아니라면 BB를 하고. B일때 허용됐으면 AA를 하고, 아니라면 BB를 하고. C일때..." 대충 이런 내용이다. 그러다보니 unauthenticated_user 처럼 구현을 하되, 마지막 함수를 하나 더 위에 구현해줌으로써, allowed_roles=[] 같이 정의된 직책을 가진 자만 그 views.pyfunction을 구현할 수 있게끔 한다.

 

차근차근 설며애호보도록 하겠다.

나머지는 비슷하나, 여기서 다른 점은 if.request.user.groups.exists()의 부분 밑에서 일 것이다. 여기서 우리는 grouprequest.user.groups.all()[0].name 으로 정의를 해준다. 또한, 만약 이 groupallowed_roles 안에 정의되어 있다면 views.py에 있는 함수를 구현시켜주고, 아니라면 HttpResponse를 통해 허용되지 않았음을 알린다.

 

따라서 views.py의 다음 함수들에 추가를 해준다:

# views.py
...
@login_required(login_url='login')
@allowed_users(allowed_roles=['admin'])
def products(request):

@login_required(login_url='login')
@allowed_users(allowed_roles=['admin'])
def customer(request, pk_customer):

@login_required(login_url='login')
@allowed_users(allowed_roles=['admin'])
def createOrder(request, pk_createOrder):

@login_required(login_url='login')
@allowed_users(allowed_roles=['admin'])
def updateOrder(request, pk_updateOrder):

@login_required(login_url='login')
@allowed_users(allowed_roles=['admin'])
def deleteOrder(request, pk_deleteOrder):
...

 

※ 중요!: 여기서 group=request.user.groups.all()[0].name은 이 project에서는 한 user당 하나의 group에 있다는 가정을 하고 하는 것이다. 하지만 예를 들어 한 사용자가 customer, seller이라는 두 그룹에 있는데 어떤 그룹에서는 customer 그룹에서는 허용이 안되고 seller 그룹에서는 허용이 될 경우에 customer이 있으면 위의 방식대로 group = request.user.groups.all()[0].name을 하게 된다면 첫번째 group, 즉, customer 만 불러와지기 때문에 허용되지 않을 것이다. 따라서 다음과 비슷한 방식을 이용해야 한다:

def allowed_users(allowed_roles=[]):
	def decorator(view_func):
		def wrapper_func(request, *args, **kwargs):
			group = None
			if request.user.groups.exists():
				group = request.user.groups.all()
...

사실상 group에 name을 넣을 필요는 없다고 생각이 된다. 하지만 정확한 방법은 아니므로 다른 방법을 찾아봐야 할 수도 있다.

 

 

하지만 이렇게 해도 애초에 사용자가 특정 group에 들어가있지 않으면 아무런 행동도 일어나지 않는다. 따라서 우리는 registerPage 함수에서 사용자가 가입을 할 때 특정 group에 들어가게끔 구현하면 된다:

# views.py
...
@unauthenticated_user
def registerPage(request):
	form = CreateUserForm()
	if request.method == 'POST':
		form = CreateUserForm(request.POST)
		if form.is_valid():
			user = form.save()
			group = Group.objects.get(name='customer')
			user.groups.add(group)
			return redirect('login')
	context = {'form':form}
	return render(request, 'accounts/register.html', context)

userform.save() 시켜주고, group = Group.object.get(name='customer')group이라는 변수 안에 customer을 저장을 시켜준다. 마지막으로, user.groups.add(group)을 통해 usercustomer이라는 group을 추가시켜준다.

 

 

※ Decorator를 만들어 특정 그룹의 사용자만 특정 행위를 허용하기(2):

 

위에서는 허용이 됐다면 A로, 허용이 안됐다면 B로 가라는 명령을 가진 decorator를 만들었다. 하지만 만약 다음과 같은 상황이면 어떨까? A라면 B의 페이지로 가게끔하고, C라면 D라는 페이지로 가게끔해야될 경우에는 allowed_users와 같은 방식은 잘 안될 것이다. 이 경우는 특히 홈페이지에서 적용이 된다. 사실상 우리가 지금 홈페이지라고 적용해놓은 페이지는 관리자만 볼 수 있도록 돼있어야 한다. 따라서 홈페이지를 접근할 경우, 관리자는 그대로 홈페이지로, 다른 일반 사용자는 user.html과 같은 곳으로 가게끔 해야한다.

 

다음과 같이 decorator를 추가해준다:

# decorators.py
...
def admin_only(view_func):
	def wrapper_func(request, *args, **kwargs):
		group = None
		if request.user.groups.exists():
			group = request.user.groups.all()[0].name
		if group == 'customer':
			return redirect('user-page')
		if group == 'admin':
			return view_func(request, *args, **kwargs)
	return wrapper_func
...

이는 이전에 우리가 했던 unauthenticated_userdecorator과 비슷하다

 

간략하게 설명하자면, group=='customer'이라면 user-page로 redirect 해주고, group=='admin' 해주는 것이다. 그리고 이는 home 함수 위에 넣어주면 된다:

# views.py
...
@login_required(login_url='login')
@admin_only
def home(request):
...

 

 

※ html의 특정 부분은 특정 그룹에게만 허용하기:

 

마지막으로 우리가 해결해야 할 부분은, 홈페이지 같은 html을 보면 위의 dashboardproducts 같은 부분이 보인다

하지만 이런 것은 애초에 navbar.html에 있어 모든 사용자가 볼 수 있을 수도 있고, 이는 관리자만 보고 접근이 가능하게끔(이건 이미 allowed_users decorator로 해결했다) 해야한다

 

따라서 이번에는 decorator가 아닌 template에서 해결해야 한다. navbar.html을 다음과 같이 추가해준다:

<!-- navbar.html -->
...
      {% if request.user.is_staff %}
      <li class="nav-item active">
        <a class="nav-link" href="{% url 'home' %}">Dashboard</a>
      </li>
      <li class="nav-item">
        <a class="nav-link" href="{% url 'products' %}">Products</a>
      </li>
      {% endif %}
  ...

여기서 if request.user.is_staff를 사용해, 만약 사용자가 staff라는 권한을 가지고 있다면 이 코드와 endif 코드 사이에 있는 html 을 접근 가능하게끔 해주고, 아니라면 아예 사용자가 안보이게끔 해준다. 실제로 새로운 사용자를 만들어서 이를 해보면 위의 dashboardproduct는 안보인다: