Django

Auth

zhelddustmq 2024. 8. 17. 23:34

01. Auth

02. 쿠키(cookie)와 세션(session)

03. Django의 Authentication System

 

01. Auth: 인증(Authentication)과 권한(Authorization)을 합쳐서 Auth

  • 인증(Authentication) : 내가 누구인지를 입증하는 것
  • 권한(Authorization) : 수행할 수 있는 자격 여부

 

02. 쿠키(cookie)와 세션(session)

 

  • HTTP 특징
    • 비연결지향 (Connectionless)
      • 한 번 요청에 대한 응답을 하면 연결이 끊어짐
    • 무상태(Stateless)
      • 연결이 끊어지면 통신이 끝나고 서로를 잊어버림
      • 모든 메세지는 독립적
    • 만약 쿠키와 세션이 없다면?
      • 이전의 요청을 기억하지 못하게 됨
      • 따라서 요청을 보낼 때 마다 매번 로그인을 해야 함

쿠키(Cookie) 🍪: 로그인 정보나 검색 기록 등을 서버가 클라에게 보내서 다음에 요청할때 다시금 로그인 등을 하지 않게끔 하는 것

서버 → 웹 브라우저에 전달하는 작은 데이터 조각

  • 유저가 웹을 방문하게 되면 서버로부터 쿠키를 전달 받음
  • Key-Value 형태로 데이터가 저장
  • 이후 동일한 서버에 보내는 모든 요청에 쿠키가 함께 전달
  • 쿠키 데이터는 유저의 로컬에 저장되는 정보

개발자 도구에 application 메뉴에서 쿠키를 볼수 있음.

 

세션(Session):  세션은 서버와 클라이언트(브라우저)간 “상태(State)”를 기억하기 위한 것

 

  • 쿠키에 내가 로그인한 유저다! 라고 적어놓고 그게 있으면 서버가 매번 로그인 한 유저라고 생각하고 데이터를 준다면 누구나 가입된 유저인 것처럼 행동할 수 있음.
  • → 쿠키는 유저의 로컬에 저장된 단순한 문자열 정보이기에 유저가 마음대로 바꿀 수 있습니다.
  • 위와같은 문제 때문에 세션이 존재
  • 세션과 쿠키가 쓰이는 방법
    1. 클라이언트가 서버에 접속하면
    2. 서버가 특정 session id를 발급하고 기억(임의의 난수)
    3. session id 전달받아 쿠키에 저장
    4. 이후 클라이언트는 해당 쿠키를 이용해서 요청
    5. 서버에서는 쿠키에서 session id를 꺼내서 검증
    6. 검증에 성공했다면 알맞은 로직을 처리
    → 쿠키에 민감한 정보를 저장할 필요 없이 session id만 저장하고 서버에서 검증하는 방식으로 사용합니다.
    → 로그인은 이러한 절차로 구현
  • 쿠키의 수명
    • 세션쿠키, Session Cookie
      • 현재의 세션이 종료되면(브라우저가 닫히면) 삭제
    • 지속쿠키, Persistent Cookie
      • 디스크에 저장되며 브라우저를 닫거나 컴퓨터를 재시작해도 남아있음
      • Max-Age를 지정하여 해당 기간이 지나면 삭제가 가능

Django의 Session 과 Auth

django에서 알아서 처리해주고 있기 때문에 직접 작성할 필요가 없음.

 

03. Django의 Authentication System: 

 

로그인 구현하기

로그인은 결국 Session을 Create하는 로직이라고 할 수 있음.

물론 Django는 이 과정을 전부 내부적으로 처리할 수 있는 기능을 제공하고 있기 때문에 우리가 session에 대한 로직을 생각하지 않아도 됨

 

Authentication Form

  • Django의 Built-in Form
  • 로그인을 위한 기본적인 form을 제공

 

  1. accounts App을 새로 만들기.
    • 계정 관련된 로직은 accounts 앱으로 하는 것이 일반적
  2. project App의 urls와 accounts App의 urls를 연결


  3. 로그인 구현하기
# accounts/urls.py
from django.urls import path
from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
]

 

# accounts/views.py (1차)
from django.shortcuts import render
from django.contrib.auth.forms import AuthenticationForm


def login(request):
    form = AuthenticationForm()
    context = {"form": form}
    return render(request, "accounts/login.html", context)
# accounts/templates/accounts/login.html
{% extends "base.html" %}

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

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

{% endblock content %}

음? 그런데 로그인해보려고 생각해보니…

  • 회원가입은?
  • 아니 그전에 ‘회원’에 대한 정의도 한적이 없는데?
  • Django는 기본적으로 모든게 갖춰져 있음. (회원가입에 대한건 다음 포스터에서)

Django의 기본 유저 모델

 

일단 superuser 를 하나 만들어서 진행

python manage.py createsuperuser # 슈퍼유저 생성
  • Django가 제공하는 Admin 기능에 접근할 수 있는 최고 권한 유저를 말함.
  • User / Staff / Superuser 로 구분

  • 강의에서는 아래 정보로 사용하기를 추천
    • username : admin
    • password : admin1234

로그인 처리를 위한 view 작성하기

# accounts/views.py  (2차)

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


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)

→ session에 대한 작업은 모두 django 내부에서 처리함

 

로그인 해보기

 

 

 

로그인 링크 달아주기 (base.html)

<!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>

 

로그아웃 구현하기

  • 로그아웃이란?
    • 결국 서버의 세션 데이터를 지우는 것
  • logout()
    • login()과 마찬가지로 logout()을 사용하면 간단하게 로그아웃을 사용할 수 있음
    • 현재 request에서 가져온 session을 사용하여 DB에서 삭제
    • 클라이언트 쿠키에서도 삭제

 

# (logout) accounts/urls.py

from django.urls import path
from . import views

app_name = "accounts"
urlpatterns = [
    path("login/", views.login, name="login"),
    path("logout/", views.logout, name="logout"),
]
#(logout) accounts/views.py

def logout(request):
    auth_logout(request)
    return redirect("index")

logout은 DB를 조작하는 요청: POST사용

 

base.html

 

 

  • 현재는 url로 접근해도 로그아웃이 동작하는 문제가 있음
    def logout(request):
        if request.method == "POST":
            auth_logout(request)
        return redirect("index")
    (logout) accounts/views.py
  • 위 코드처럼 view에서 막아도 되지만 매번하기 귀찮음

HTTP 요청을 처리하는 다양한 방법

https://docs.djangoproject.com/en/4.2/topics/http/decorators/

 

View decorators | Django documentation

The web framework for perfectionists with deadlines.

docs.djangoproject.com

 

Django가 HTTP요청을 처리하는 2가지 방법

 💡 Django shortcut functions

https://docs.djangoproject.com/en/4.2/topics/http/shortcuts/#module-django.shortcuts

  • render()- 템플릿을 랜더링해서 전달
  • redirect() - 특정 경로로 요청을 전달
  • get_object_or_404()
    • get을 호출한 후 객체가 없다면 404 에러를 raise하여 404 페이지로 이동
  • get_list_or_404()
    • filter를 호출한 후 빈 리스트라면 404 에러를 raise하여 404페이지로 이동
  • 존재하지 않는 게시글을 조회한다면
    http://127.0.0.1:8000/articles/9999 로 들어가면 아래의 화면이 나옴

 

  • 상태코드는 총 5가지 종류가 있음

 

400번대 코드, 즉 403, 404와 같은 코드라면 클라이언트의 요청에 문제가 있음을 나타내고

500번대 코드는 서버 내부에 문제가 생겨 요청을 처리할 수 없다는 것을 나타냄

200번대 코드는 이상 없음

존재하지 않는 게시물을 조회하려고 했기 때문에 클라이언트의 요청에 문제가 있음을 나타내는 400번대 상태코드(404 Not Fount)가 더 적절.

 

수정해봅시다!

처리 완료된 페이지

View Decorators

  • 여러가지 다양한 HTTP 기능을 제공하기 위한 데코레이터를 제공
  • require_http_methods()
    • view 함수를 특정한 method 요청에 대해서만 허용
  • require_POST()
    • POST 요청만 허용
  • 적용해보기 

 

 

  • Template with Auth
    • Auth 기능을 Template에서 활용
      • template으로는 우리가 context를 넘기지 않아도 자동으로 넘어가는 context들이 존재
      • request.user 도 그 중에 하나로 템플릿을 랜더링할때 현재 로그인한 사용자를 나타내는 auth.User 클래스의 인스턴스 또는 AnonymousUser 인스턴스를 request.user로 접근할 수 있음
    • base.html에 소소하게 적용해보기

  • 접근 제한하기
    •  로그인이 된 유저와 아닌 유저가 이용할 수 있는 기능에 접근 제한 두기
      • is_authenticated 속성 사용하기
      • @login_required 사용하기
    • is_authenticated
      1. base.html
# (is_authenticated) base.html
<!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">
        {% if request.user.is_authenticated %}
            <h3>안녕하세요, {{ user }}님</h3>
            <form action="{% url 'accounts:logout' %}" method="POST">
                {% csrf_token %}
                <input type="submit" value="로그아웃">
            </form>
        {% else %}
            <a href="{% url 'accounts:login'%}">로그인</a>
        {% endif %}
    </div>

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

</body>
</html>

 

# accounts/views.py

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

 

articles/articles.html

  • login_required
    • 로그인이 되어있지 않은 상태에서 접근하면 settings.LOGIN_URL 에 설정된 경로로 이동
      • 기본 값은 /accounts/login/
    • 로그인이 되어있으면 view 로직을 실행
    • 로그인 성공시 이전 페이지로 자동으로 이동
      • 쿼리스크링에 next 로 저장
        • 별도 처리 안해주면 지정한 경로로 이동

accounts/views.py

 

accounts/login.html

next 가 있는 현재 url을 사용하라고 비워주기

 

# articles/views.py
from django.contrib.auth.decorators import login_required

  • /articles/create/ 로 강제 접근을 하면
  • /accounts/login/?next=/articles/create/ 로 리다이렉트 됨
  • update, delete도 모두 적용해주기
# (update, delete) accounts/views.py

from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods


@login_required
@require_POST
def delete(request, pk):
    article = Article.objects.get(pk=pk)
    article.delete()
    return redirect("articles:articles")


@login_required
@require_http_methods(["GET", "POST"])
def update(request, pk):
    article = Article.objects.get(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)


def data_throw(request):
    return render(request, "articles/data_throw.html")

 

 

한 번 테스트를 해보면 글 삭제시 아래와 같은 에러가 발생

  1. 비로그인상태에서 삭제 클릭
  2. 로그인 화면으로 리다이렉트
  3. next=<삭제 url>
  4. 로그인 성공
  5. <삭제 url>로 리다이렉트 (GET)
  6. 하지만 우리의 view는 GET을 허용하지 않음!
  • 해결
    • login_required 를 지우고 안쪽 로직에서 분리하도록 처리해서 해결 가능
# (문제 해결) accounts/views.py

@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")

직접 접근하면 405 에러지만 이건 벗어난 flow에서 나오는 것.이전에 우리가 설계한 flow 자체가 에러였던 것