본문 바로가기

매일 TIL

[내일배움캠프 8-4일] 로그인&로그아웃 구현하기, 404 처리, 데코레이터

Django Authentication System

만약 유저가 id와 pw를 입력한다면 서버는 어떤 과정을 거쳐야 할까?

1. 입력한 ID가 우리 DB에 있는지 확인

2. 입력한 pw가 일치하는지 확인

3. 그 유저가 몇번인지 판독

4. Session Table로 가서

5. 난수를 생성

6. Session Table에 난수:유저 형태로 저장

7. 생성한 난수를 쿠키에 담아 유저에게 전송

 

django에서 이 모든 과정은 login() 이라는 함수 하나를 통해 내부적으로 처리된다.

즉 내부적으로 session을 사용해서 user정보를 제작해준다는 것.


로그인 구현하기

우선 accounts 앱을 만들고, 이를 my_first_pjt와 연결해야 함.(할 수 있다. 천천히 해보면 됨)

accounts 파일 만들기 -> settings에 앱 등록 -> accounts 앱에 urls.py 생성 -> my_first_pjt에서 include -> urls 기본 틀 잡기(app_name까지)

 

이후 햄버거 구조를 만든다. 즉 templates 안쪽에 다시 accounts 폴더를 만들어야 함.

이 안쪽에 템플릿들이 들어가면 된다.

 

그러면 로그인 창을 만들어보자

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('login/', views.login, name='login'),
]

urls.py

from django.shortcuts import render
from django.contrib.auth.forms import AuthenticationForm

# Create your views here.
def login(request):
    form = AuthenticationForm()
    context = {'form': form}
    return render(request, 'accounts/login.html', context)

views.py

장고는 Authentication Form을 제공하기 때문에 import 해서 사용하면 됨.

늘 하던대로 context에 담음.

{% extends 'base.html' %}

{% block content %}
<h1>로그인</h1>

<form action="{% url 'accounts:login' %}" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="로그인">
</form>

{% endblock content %}

login.html

로그인을 하는 과정에서 데이터를 생성하는 것이므로 POST 방식

나머지는 하던대로 하면 됨.

 

어라 그런데 로그인 창만 만들면 뭐함? 유저 데이터가 있는 테이블을 먼저 만들어야 하지 않나?

장고는 이미 다 해주고 있다.

squlite로 DB를 확인해보면,

이미 auth_user라는 테이블이 존재함.

우리는 필요에 따라 이를 상속하여 커스텀한 뒤 사용하면 되는 것.

 

자 그럼 우선 superuser를 하나 생성해보자

createsuperuser로 생성하고 아래과정을 채워주면 된다.

 

그런데 우리는 로그인 정보를 받아 POST 요청 했을 경우 어떻게 진행할지 view에서 구현하지 않음.

하던 것처럼 구현해보자.

from django.shortcuts import render, redirect
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth import login as auth_login

def login(request):
    if request.method == 'POST':
        form = AuthenticationForm(data=request.POST)
        if form.is_valid:
            auth_login(request, form.get_user())
            return redirect('articles:index')

    else:
        form = AuthenticationForm()
    context = {'form': form}
    return render(request, 'accounts/login.html', context)

구현한 코드를 보면 결국 login 로직은 auth_login(request, form.get_user()) 이 한줄로 끝.

request를 첫번째 인자로, form.get_user()를 두번째 인자로 받음.

request를 받아야 여기로 들어온 쿠키를 열어서 이것저것 할 수 있음.

어떤 user를 로그인 처리할지를 form.get_user()로부터 받는 것.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>

    <div class="navbar">
        <a href="{% url 'accounts:login' %}">로그인</a>
    </div>

    <div class="container">
        {% block content %}
        {% endblock content %}
    </div>

</body>
</html>

base.html

로그인 페이지로 이동할 수 있는 a태그를 만들고 싶은데, 모든 페이지에 나오도록 만들고 싶다면

우리가 공통으로 상속받고 있는 base.html에서 형식을 바꿔주면 된다.

이러면 로그인 구현 끝.


로그아웃 구현하기

from django.urls import path
from . import views

app_name = 'accounts'

urlpatterns = [
    path('login/', views.login, name='login'),
    path('logout/', views.logout, name='logout'),
]

하던대로

from django.contrib.auth import logout as auth_logout
def logout(request):
    auth_logout(request)
    return redirect('articles:index')

하면된다

여기서도 auth_logout(request) 이 한줄로,

request를 받고 확인해서 session 테이블에서 정보 삭제 + 쿠키의 session id 삭제까지 알아서 처리해준다.

<body>

    <div class="navbar">
        <a href="{% url 'accounts:login' %}">로그인</a>

        <form action="{% url 'accounts:logout' %}" method="POST">
            {% csrf_token %}
            <input type="submit" value="로그아웃"></input>
        </form>
    </div>

    <div class="container">
        {% block content %}
        {% endblock content %}
    </div>

</body>

로그아웃 역시 모든 페이지에서 이동할 수 있는 버튼이 필요 만들어보자. 당연히 base.html에서 처리.

그런데 로그아웃은 DB에서 DELETE하는 것이기 때문에 POST 요청에 해당됨.

html에서 POST는 form으로 처리.(로그인은 로그인창을 조회하는 GET 요청이기 때문에 form이 필요없었던 것)


404 처리

articles/999 해보자. 당연히 페이지 안나옴. 해당 pk에 해당하는 페이지가 없기 때문에.

그런데 요청한 것은 클라이언트인데 서버 잘못이라는 의미의 500번대가 출력됨 -> 처리해주자

article = Article.objects.get(pk=pk)
def article_detail(request, pk):
    article = get_object_or_404(Article, pk=pk)
    context={'article':article}
    return render(request, 'articles/article_detail.html', context)

articles 앱의 views.py

aritcle = Article.objects.get(pk=pk) 문장을 article = get_object_or_404(Article, pk=pk) 로 바꿔주면 됨.

참고로 get_object_or_404는 django.shortcuts에 있으므로 import 해준다.


데코레이터

def logout(request):
    if request.method == 'POST':
        auth_logout(request)
    return redirect('index')

데코레이터가 없는 현실

 

데코레이터 사용해보자.

from django.views.decorators.http import require_POST

우선 require_POST import

@require_POST
def logout(request):
    auth_logout(request)
    return redirect('index')

데코레이터 사용. 이제 조건문은 필요없음.

 

from django.views.decorators.http import require_POST, require_http_methods
@require_http_methods(['GET', 'POST'])
def login(request):
    if request.method == 'POST':
        form = AuthenticationForm(data=request.POST)
        if form.is_valid:
            auth_login(request, form.get_user())
            return redirect('articles:index')
    else:
        form = AuthenticationForm()
       
    context = {'form': form}
    return render(request, 'accounts/login.html', context)

요런 식으로 사용 가능.

require_http_methods는 리스트 형태로 받음.

리스트 안에 있는 요청 말고는 다 튕겨내겠다는 것.


Template with Auth

뭔가 욕심이 난다.

화면에 내가 로그인 되어있는지 표시해주는 기능이 있으면 좋겠다.

즉 Auth 기능을 템플릿에서 사용하자는 것.

로그인 상태이면 로그아웃 버튼만, 로그아웃 상태이면 로그인 버튼만 나오도록 해보자.


is_authenticated

<div class="navbar">

        {% if request.user.is_authenticated %}
        <h3>Hello, {{ request.user.username }}</h3>
        <form action="{% url 'accounts:logout' %}" method="POST">
            {% csrf_token %}
            <input type="submit" value="로그아웃"></input>
        </form>

        {% else %}

        <a href="{% url 'accounts:login' %}">로그인</a>

        {% endif %}

    </div>

조건문과 is_authenticated 속성을 사용하여 처리.

@require_POST
def logout(request):
    if request.user.is_authenticated:
        auth_logout(request)
    return redirect('index')

accounts 앱의 views.py

{% extends 'base.html' %}

{% block content %}
<h2>Articles</h2>

{% if request.user.is_authenticated %}
<a href="{% url 'articles:create' %}">
    <button>새로운 글 작성</button>
</a>
{% else %}
<a href="{% url 'accounts:login' %}">로그인하고 글 작성하기</a>
{% endif %}

{% for article in articles %}
<a href="{% url 'articles:article_detail' article.pk %}">
    <p>[ {{ article.pk }} ] {{ article.title }}</p>
</a>

{% endfor %}
{% endblock content %}

articles.html

이런 식으로도 사용할 수 있음.

즉, 로그인 한 유저와 아닌 유저가 이용할 수 있는 기능에 접근 제한을 두기 위해 사용.


login_required

from django.contrib.auth.decorators import login_required

우선 import.

@login_required
def create(request):
    if request.method == 'POST':
        form = ArticleForm(request.POST)
        if form.is_valid():
            article = form.save()
            return redirect('articles:article_detail', article.pk)
    else:
        form = ArticleForm()
   
    context = {'form': form}
    return render(request, 'articles/create.html', context)

이제는 url로 직접 articles/create로 접근하여 새 글을 작성하려 해도 불가능하다.

바로 로그인 페이지로 보내버림.

이후 로그인하면 index로 보내짐.

근데 뭔가 이상하다.

새 글을 작성하려고 함 -> 실패 -> 로그인 화면으로 이동됨 -> 로그인 성공 -> index 페이지로 이동(???)

index가 아닌 새 글을 작성할 수 있는 페이지, 즉 articles/create로 이동시켜줘야 맞는거 아닌가?

@require_http_methods(['GET', 'POST'])
def login(request):
    if request.method == "POST":
        form = AuthenticationForm(data=request.POST)
        if form.is_valid():
            auth_login(request, form.get_user())
            next_url = request.GET.get('next') or 'index'
            return redirect(next_url)
    else:
        form = AuthenticationForm()
    context = {"form": form}
    return render(request, "accounts/login.html", context)

accounts 앱의 views.py

next_url을 만들어서 request.GET.get을 통해 next를 가져옴. 만약 없다면 index.

{% extends 'base.html' %}

{% block content %}
<h1>로그인</h1>

<form action="" method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="로그인">
</form>

{% endblock content %}

login.html

action 자리를 비워놓으면 현재 내가 들어온 url로 데이터를 다시 보냄(HTTP form 쿼리 스트링때 했었던 거)

요렇게 하면 유저가 정상적인 순서로 진행할 수 있다.

 

update도 동일하게 해주자

@login_required
@require_http_methods(['GET', 'POST'])
def update(request, pk):
    article = get_object_or_404(Article, pk=pk)
    if request.method == 'POST':
        form = ArticleForm(request.POST, instance=article)
        if form.is_valid():
            article = form.save()
            return redirect('articles:article_detail', article.pk)
    else:
        form = ArticleForm(instance=article)
    context = {
        'form': form,
        'article': article,
    }
    return render(request, 'articles/update.html', context)

 

delete도 동일하게 해주다가 문제 발생

비로그인 상태로 글 삭제 시도 -> 로그인 페이지로 이동 -> next가 delete url이 되고 -> 로그인을 성공하면 -> delete url로 GET 요청 -> 이때 우리 delete view는 POST만 허용하므로 문제가 생긴다.

@require_POST
def delete(request, pk):
    if request.user.is_authenticated:
        article = get_object_or_404(Article, pk=pk)
        article.delete()
    return redirect('articles:articles')

login_required는 사용하지 못함.

안쪽 로직에서 조건문과 user.is_authenticated를 사용하여 처리해주면 해결


오늘의 회고

힘드르

내일의 목표는 진도 마저 나가기...