Lina's Toolbox

Django Auth 본문

Auth

  • 웹 개발에 빼놓을 수 없는게 바로 Auth입니다!
  • → 당연히 django도 제공하고 있습니다. 🙂
  • settings.py를 살펴봅시다.

(settings.py의 미들웨어)

request → response나갈때 그냥 나가는게 아닌

미들웨어들을 거친 후 response로 나간다.

내가 만약 무조건 거치게 만들고싶은 과정이 있다면

커스텀 미들웨어를 만들어 추가해줄수 있다.

 

  • django.contrib.auth → 인증 핵심 로직과 관련 기본 모델
  • django.contrib.contenttypes → 사용자의 모델과 권한을 연결
💡 아, 그런가보다~ 하고 넘어가도 되는 부분입니다.

 

인증(Authentication)권한(Authorization)을 합쳐서 Auth라고 대개 인증시스템이라고 명명합니다.

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

쿠키(cookie)와 세션(session)

HTTP

  • HTTP 이야기가 계속 나오는 이유
    • 웹은 HTTP 방식을 사용해서 통신을 주고받고 있기때문입니다.
      (현대의 모든 웹에서의 데이터 교환의 기본 - 우리는 그 위에서 장고를 쓰고 있는 것!)
  • HTTP 특징
    • 비연결지향 (Connectionless)
      • 한 번 요청에 대한 응답을 하면 연결이 끊어짐
    • 무상태(Stateless)
      • 연결이 끊어지면 통신이 끝나고 서로를 잊어버림
      • 모든 메세지는 독립적  
        (한번 보냈던 메시지는 그다음 보내는 메세지와 전혀관련이 없다. - 무상태)
    • 만약 쿠키와 세션이 없다면?
      • 이전의 요청을 기억하지 못하게 됩니다.
      • 따라서 요청을 보낼 때 마다 매번 로그인을 해야합니다.
      • 이렇게 되면 너무 불편하겠죠?

→ 로그인한 상태를 유지하려면?

→ 서로를 기억하기 위해 필요한 것이 바로 ‘쿠키와 세션’

 

 

쿠키(Cookie) 🍪

  • 서버 → 웹 브라우저에 전달하는 작은 데이터 조각입니다.
    • 유저가 웹을 방문하게 되면 서버로부터 쿠키를 전달받습니다.
  • Key-Value 형태로 데이터가 저장됩니다.
  • 이후 동일한 서버에 보내는 모든 요청에 쿠키가 함께 전달됩니다.
  • 쿠키 데이터는 유저의 로컬에 저장되는 정보입니다.
  • 웹브라우저에서 관리자 도구 열고, application탭에서 확인 가능!
    심지어 수정, 삭제도 가능!! (ex. 장바구니 담겨있던게 사라짐)
    쿠키는 조작이 너무 쉽다!
    → 보안이슈

그래서 이 귀여운 쿠키가 🍪

쿠키는 문자열로 이루어진 유저의 작은 데이터조각 !
장바구니 기능 (쿠키에 어떤 물건을 담았었는지 기억)
최근 검색한 상품들(쿠키에 기억) → 서버에서 받아서 맞춤형 광고
오늘 다시보지 않기 (쿠키에 기억)

→ 어? 그럼 내가 뭐했는지 쿠키에 싹 저장해두었다가, 서버가 가져가네 …? 🤔

 

조금은 무서운(?) 이야기

  • 이 쿠키가 활용되는 가장 중요한 분야가 바로 ‘광고시장’입니다.
  • 검색 기록을 추적해서 쉽게 유저별 맞춤형 광고를 할 수 있기 때문입니다.
  • 그렇기 때문에 쿠키에 대해 개인정보이슈에 대해 논의가 이루어지고 있습니다!
    → 광고 시장이 크게 바뀔 수 있는 변화
    → 구글 크롬도 쿠키에 대한 제제를 발표 (2024년 발효 예정)
💡 한 줄 정리
웹 페이지에 요청을 보내면 서버가 쿠키를 함께 전달하고
이후부터는 같은 서버에 보내는 모든 요청에 쿠키를 함께 담아서 요청을 보내게 된다.

 

 

세션(Session)

만약 쿠키 🍪만 있다면 어떤 일이 일어날까요?

쿠키에 내가 로그인한 유저다 ! 라고 적어놓고 그게 있으면 서버가 매번 로그인 한 유저라고 생각하고 데이터를 준다면 누구나 가입된 유저인 것처럼 행동할 수 있습니다.
→ 쿠키는 유저의 로컬에 저장된 단순한 문자열 정보이기에 유저가 마음대로 바꿀 수 있습니다.

💡 세션은 서버와 클라이언트(브라우저)간 '상태(State)'를 기억하기 위한 것입니다!

 

  • 세션과 쿠키가 쓰이는 방법은 다음과 같습니다.
    1. 클라이언트가 서버에 접속하면
    2. 서버가 특정 session id(임의의 난수)를 발급하고 기억(DB나 메모리에서 기억)
    3. 클라이언트에 SID를 보냄. 클라이언트는 session id 전달받아 쿠키에 저장
    4. 이후 클라이언트는 다음 요청을 할 때, 해당 쿠키(session id)를 이용해서 요청
    5. 서버에서는 쿠키에서 session id를 꺼내서 검증(실제로 해당 SID있는 지 확인)
    6. 검증에 성공했다면 알맞은 로직을 처리
    → 쿠키에 민감한 정보를 저장할 필요 없이 session id만 저장하고 서버에서 검증하는 방식으로 사용합니다.
    → 로그인은 이러한 절차로 구현됩니다.

 세션은 쿠키를 이용한 기술!

 

💡 쿠키는 영원한 것은 아니고 수명이 있다!

 

  • 쿠키의 수명
    • 세션쿠키, Session Cookie
      • 현재의 세션이 종료되면(브라우저가 닫히면) 삭제됩니다.
    • 지속쿠키, Persistent Cookie
      • 디스크에 저장되며 브라우저를 닫거나 컴퓨터를 재시작해도 남아있습니다.
      • Max-Age를 지정하여 해당 기간이 지나면 삭제가 가능합니다.

 

Django의 Session 과 Auth

다 처리를 해주고 있다 !

django에서 알아서 처리해주고 있기 때문에 직접 작성할 필요가 없습니다!


Django의 Authentication System

로그인 구현하기

💡 로그인은 결국 Session을 Create하는 로직이라고 할 수 있습니다!

로그인을 하려면,
form에서 id와 pw를 입력받고
→ DB에서 유저판독
→ 세션테이블가서
→ 난수생성
→ 세션테이블에 기억
이러한 과정이 필요함!
하지만 장고에서는 login()함수만 쓰면 이걸 다 해줌!!

 Django는 이 과정을 전부 내부적으로 처리할 수 있는 기능을 제공하고 있기 때문에
우리가 이러한 session에 대한 로직을 생각하지 않아도 됩니다. (Django 👍)
우리는 가져다 쓰기만 하면 됨!
→ 우리가 프레임워크를 쓰는 이유 (생산성⬆️)

 

Authentication Form

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

login()

  • 개발자가 직접 구현하지 않아도 login()함수 하나만 사용하면 됩니다!
  • 사용자 로그인 처리를 해주고 내부적으로 session을 사용해서 user 정보 저장합니다. (Django 👍)

사용해보자

  1. accounts App을 새로 만들어 주세요.
    • 계정 관련된 로직은 accounts 앱으로 하는 것이 일반적입니다.
    • 이제 생성하고 등록하는 말은 굳이 또 하지 않을게요. (어라 이미 한거아닌가…)
  2. project App의 urls와 accounts App의 urls를 연결해 주세요.

 

  1. 로그인 구현하기
    • 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의 기본 유저 모델 저는 이것 말고도 더 많은 정보가 필요한데요 ?
→ 이 유저모델을 ‘확장’하면 되겠죠 🙂

이것은 클래스임!

커스텀하고 싶다면

이것을 상속하여 확장시킨 나만의 커스텀 유저 모델을 만들 수 있다.


그러면 회원으로 등록은 어떻게 하는건가요?
회원 가입 역시 Django가 제공하는 여러기능이 있지만 이건 다음으로!
지금은 superuser 를 하나 만들어서 진행하겠습니다. 


superuser란?

createsuperuser

python manage.py createsuperuser # 슈퍼유저 생성

 

  • Django가 제공하는 Admin 기능에 접근할 수 있는 최고 권한 유저를 말합니다.
  • User / Staff / Superuser 로 구분

User : 구매자

Staff: 상품관리자

Superuser : 어드민

패스워드 입력할 때 안보이는 것이 정상임!

  • 강의에서는 아래 정보로 사용하기를 추천합니다. (우리는 망각이 특기니까 까먹었을 땐 현재 페이지를 보면 알 수 있겠죠?)
    • username : admin
    • password : admin1234

 

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

 

accounts/views.py (2차)

# accounts/views.py

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 내부에서 처리합니다.

 

get으로 들어오면: 로그인 화면 보여줌

post: 로그인 처리 후 다른 페이지로 리다이렉트 해줘야함

 

login 임포트해줘야한다. → from django.contrib.auth import login

우리의 login함수와 이름 같으므로  as auth_login해준다.

 

form.get_user() 도 장고가 제공해주는 기능!

 

auth_login: 장고가 제공해주는 로그인 함수

request를 첫번째 인자로 받는다 (리퀘스트를 받아야 리퀘스트로 들어온 해당 브라우저의 쿠키를 받아 처리가 가능하겠죠?)

 

→ 한줄로 로그인 처리가 끝난다!

→ 장고의 힘 👍🏻

 

 

로그인 해보기

우리가 만든 화면에서 로그인해보면 session id가 쿠키에 생겼다!

 

DB에 장고 세션 테이블을 열어보면 해당 세션이 있다.

무슨값인지는 우리도 모른다. 장고가 알아서 내부적으로 암호화처리하여 어떤 유저인지의 정보가 있다.

이 밸류 값을 가지고 장고가 알아서 어떤 유저인지 확인하는 것.

이 세션 아이디를 날리면 로그아웃처리된다.

 

 

로그인 링크 달아주기 (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>

<body>안에 <div>태그 안에 넣는게 일반적임.

(나중에 css로 조절할때 이 div만 조정하면 되므로)


 

로그아웃 구현하기

  • 로그아웃이란?
    • 결국 서버의 세션 데이터를 지우는 것입니다!
      (request 까서 쿠키에 세션아이디 있으면 찾아서 지우고 쿠키에서도 세션아이디 지우고...)
  • logout()
    • login()과 마찬가지로 logout()을 사용하면 간단하게 로그아웃을 사용할 수 있습니다.
      (→ 장고가 해준다!)
    • 현재 request에서 가져온 session을 사용하여 DB에서 삭제합니다.
    • 클라이언트 쿠키에서도 삭제합니다.

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"),
]

 

accounts/views.py

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

import해주고 한줄컷

  • 생각해보자그럼 어떤 방법으로 요청해야할까요? get? post?
  • logout은 DB를 조작하는 요청입니다. → POST
    → 로그아웃 버튼은 a태그가 아닌  form 으로 구현해야한다.

base.html

 

🧐 그런데 말입니다

현재는 url로 접근해도 로그아웃이 동작하는 문제가 있습니다.
(버튼 눌러서 로그아웃하지않아도 url에서 /logout/ 를 붙여 들어가면 로그아웃이 되어버린다.)

아, 그거 view에서 막아주면 되죠 ~ 🙂 

(logout) accounts/views.py
def logout(request):
    if request.method == "POST":
        auth_logout(request)
    return redirect("index")​

post일 때만 처리하도록 막아주었다!



→ 우리는 앞으로 많은 view를 작성해야하는데, 더 편하게 할 수 없을까요?

 

 

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

📌 공식문서 참조
https://docs.djangoproject.com/en/4.2/topics/http/decorators/

 

  • 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 로 들어가면 아래의 화면이 나옵니다.

 

💡 상태코드
하이라이트 된 부분에 500이라는 숫자가 보이죠?
이것은 상태코드로 요청에 대한 서버의 응답상태를 말합니다.

상태코드는 총 5가지 종류가 있습니다.
여기서는 간단하게만 설명해드릴게요.

400번대 코드, 즉 403, 404와 같은 코드라면 클라이언트의 요청에 문제가 있음을 나타내고
500번대 코드는 서버 내부에 문제가 생겨 요청을 처리할 수 없다는 것을 나타냅니다.

존재하지 않는 게시물을 조회하려고 했기 때문에
클라이언트의 요청에 문제가 있음을 나타내는 400번대 상태코드(404 Not Fount)가 더 적절하겠죠?
(상태코드도 mdn에 검색하면 나옵니다!)

 

 

수정해봅시다!

  • articles/views.py

처리 완료된 페이지

 

try: 
	article = Article.objects.get(pk=pk)
except Article.DoesNotExist:
	return redirect ("articles:articles")

이렇게 적어줘도 되겠지만,

article = get_object_or_404(Article, pk=pk) 이렇게만 수정해주면 더 편하겠죵?

참고로 사용하려면 import 해줘야함.


http요청 method 는 get, post말고도 다양하게 있다.

if request.method == "POST"
	pass
elif request.method == "GET"
	pass
else:
	pass

 

이렇게 다해주려면 너무 귀찮다!

→ 데코레이터를 사용하자!

 

💡 View Decorators

  • 여러가지 다양한 HTTP 기능을 제공하기 위한 데코레이터를 제공
    (데코레이터: 함수를 한번 감싸서 실행 → 함수 전,후에 실행함)
  • require_http_methods()
    • view 함수를 특정한 method 요청에 대해서만 허용
      (인자로 get, post등 요청 종류 써주면 나머지는 알아서 튕겨내준다.)
  • require_POST()
    • POST 요청만 허용
  • 적용해보기

(logout) accounts/views.py

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

이제 if request.method == POST 필요없다.

지워주고 @데코레이터만 써주면됨!


 

 

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

 

 

로그인이 되어있을때/ 안되어있을때 마다 다르게 화면에 출력하려면?

(ex. 로그인 했을때는 로그아웃만 보이게, 로그인안햇을때는 로그인이 보이게)

 

  • 접근 제한하기
    • 로그인도 있겠다, 로그인이 된 유저와 아닌 유저가 이용할 수 있는 기능에 접근 제한을 둘 수 있으면 좋겠죠?
      • is_authenticated 속성 사용하기
      • @login_required 사용하기
    • is_authenticated
      1. 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>

request.user: 로그인한 사용자의 객체를 가지고있다. (auth.User의 인스턴스)

로그인하면 admin, 로그아웃하면 annonymous user로 보인다.

 

 

2. accounts/views.py

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

 

 

3. articles/articles.html

 

 

로그인한 유저만 글을 쓰게 하고 싶다?

해당 뷰에 로그인한 유저만 접근하도록 하려면?

 

if not request.user.is_authenticated:

     return redirect("accounts:login")

→ 로그인한 유저만 처리할 수 있게

→ 그런데 필요한 모든 메서드에 이걸 해주려면 귀찮을 것이다.

 

  • @login_required
    • 이 데코레이터를 붙여준 함수는 로그인이 되었을때만 실행됨
    • 로그인이 되어있지 않은 상태에서 접근하면 settings.LOGIN_URL 에 설정된 경로로 redirect 시킵니다.
      • 기본 값은 /accounts/login/
        (→ 장고로 유저관련 기능은 accounts앱으로 만드는것이 관행인 이유)
        → 이제 로그인 안한 상태에서, url로 articles/create하면, 로그인페이지로 이동시켜준다.
    • 로그인이 되어있으면 view 로직을 실행합니다.
    • 로그인 성공시 이전 페이지로 자동으로 이동합니다.
      → 여기서는, 로그인하면 인덱스 페이지로 이동한다.
      → 글작성을 하고싶었는데 인덱스로 가니 불편하다..
      /accounts/login/?next=글작성페이지(/articles/create/)를 해줘야할 것 같은데
      장고에서 이 기능을 제공한다!
      • 쿼리스트링에 next 로 저장해줍니다.
        • 별도 처리 안해주면 지정한 경로로 이동합니다.
          next_url = request.GET.get("next") or(만약에 없으면) "index"
        • 처리해주자 !
          • accounts/views.py

 

  • accounts/login.html

아직 form의 action 때문에 아직도 인덱스로 간다. (내 사이트로 다시 요청하기때문에)

쿼리스트링 없는 /login/account로 보내기 때문에 인덱스로 갔던것.

action을 지정하지 않거나, next가 있는 현재 url을 사용하라고 비워주면됨

 

  • 적용하기
    • articles/views.py
from django.contrib.auth.decorators import login_required

 

  • /articles/create/ 로 강제 접근을 하면
  • /accounts/login/?next=/articles/create/ 로 리다이렉트됩니다.
  • 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 자체가 에러였던 것입니다.

'스파르타 내일 배움 캠프 AI 웹개발 과정 > Django framework' 카테고리의 다른 글

배포하기  (0) 2024.09.04
API 문서화  (0) 2024.09.04
장고 기초 이해도 테스트  (1) 2024.09.02
Django의 URL namespace  (0) 2024.09.02
Django Form  (0) 2024.08.31