13. Login

조재훈·2022년 7월 28일
0

Clone_Airbnb

목록 보기
30/31
post-thumbnail

로그인 기능을 구현할 것이다.

1) Login 페이지 연결

users 앱 내에 구현하도록 하자.
users - urls.py

from django.urls import path
from . import views

urlpatterns = [path("login", views.LoginView.as_view(), name="login")]

기본 View를 사용시에는 get과 post 두 가지만 가지는데 기본적으로 모든 HTTP 메소드들을 가지게 되는 것이다.
users - views.py

from django.views import View

class LoginView(View):
    def get(self, request):
        pass
    
    def post(self, request):
        pass

이 CBV 방식은 FBV기반으로 할때 아래와 같다.

def login_view(request):
    if request.method == 'GET':
        pass
    elif request.method == 'POST':
        pass

이 url을 config의 urls.py에 추가하자.
config - urls.py

...

urlpatterns = [...
    path("users/", include("users.urls", namespace="users")),
]

...

app_name을 설정하라는 경고가 뜬다.

users - urls.py

from django.urls import path
from . import views

app_name = "users"

urlpatterns = [path("login", views.LoginView.as_view(), name="login")]

하지만 login을 눌러도 아무데로도 안간다.

nav에서 <li>의 href가 '#'으로 되어있던걸 users:login으로 고쳐준다.
templates - partials - nav.html

<a href="{% url 'core:home' %}">Hairbnb</a>
<ul>
    <li><a href="{% url 'users:login' %}">Login</a></li>
</ul>

그러면 일단 url이 login 페이지로 설정은 된다. 단, 아직은 return하는 http요소가 없다.

templates - users - login.html

{% extends "base.html" %}


{% block page_title %}
    Log In
{% endblock page_title %}


{% block search-bar %}
{% endblock search-bar %}


{% block content %}

    <h1>Hello</h1>

{% endblock content %}

users - views.py

from django.views import View
from django.shortcuts import render


class LoginView(View):
    def get(self, request):
        return render(request, "users/login.html")

    def post(self, request):
        pass


2) form 작성

로그인시 장고는 username, email, password를 요구하지만 여기서는 username대신 email로 일원화할 것이다.
user - forms.py

from django import forms


class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField()

이제 view에서 form을 불러오자
users - views.py

from django.views import View
from django.shortcuts import render
from . import forms


class LoginView(View):
    def get(self, request):
        form = forms.LoginForm()
        return render(request, "users/login.html", context={"form": form})

    def post(self, request):
        pass

templates - users - login.html

...

{% block content %}

    <h1>Hello</h1>

    {{form.as_p}}

{% endblock content %}



근데 비밀번호가 안가려진다

forms.py를 약간 수정하자

from django import forms


class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)



templates - users - login.html

{% block content %}

    <form>
        {{form.as_p}}
        <button>Login</button>
    </form>

{% endblock content %}

아니? url에 비밀번호가 왜 나오냐

이건 GET 메소드로 하면 안된다.
templates - users - login.html

{% block content %}

    <form method="POST" action="{% url 'users:login' %}">
        {{form.as_p}}
        <button>Login</button>
    </form>

{% endblock content %}

CSRF 에러가 떴다. (CSRF : Cross Site Request Forgery, 사이트간 요청 위조) 웹사이트에 로그인하면 웹사이트가 쿠키를 주는데, 브라우저가 백엔드로 쿠키를 보내는 방식은 도메인에 의해 이루어진다. 그러면 그 다음에 로그인할 때마다 그 쿠키를 사이트에 주게 된다. 문제는 어떤 악의적인 사용자에 의해 이 쿠키가 탈취당할수 있다.

이럴땐 {% csrf_token %}을 쓰자.
https://docs.djangoproject.com/en/4.0/ref/csrf/

templates - users - login.html

{% block content %}

    <form method="POST" action="{% url 'users:login' %}">{% csrf_token %}
        {{form.as_p}}
        <button>Login</button>
    </form>

{% endblock content %}

보면 input의 type이 hidden으로 바뀌고 value에도 이상한 토큰값이 들어갔다. 이 토큰은 적절한 웹사이트에서 post request가 들어왔는지 검증하는 용도이다. 내가 접속하려는 사이트가 아닌 다른 사이트에서 request가 왔는지 걸러낼 수 있다면 쿠키 도용으로 인한 사고를 막을 수 있다.

post를 작성하고서 출력해보자.
users - views.py

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm()
        return render(request, "users/login.html", context={"form": form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        print(form)

users - views.py

from django.views import View
from django.shortcuts import render
from . import forms


class LoginView(View):
    def get(self, request):
        form = forms.LoginForm(initial={"email": "123@123.com"})
        return render(request, "users/login.html", context={"form": form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        print(form.is_valid())
        return render(request, "users/login.html", context={"form": form})

여기서 form.is_valid()의 경우 메인 페이지에서 로그인해보면 콘솔에 아래와 같이 뜬다. 사실 저렇게 뜬다고 데이터가 맞는건 아니니 아직 큰 상관은 없다.

form에 있는 요소들을 검사하고 싶으면 요소 이름 앞에 clean을 붙인다.
users - forms.py

from django import forms


class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_email(self):
        print("clean email")

    def clean_password(self):
        print("clean password")



양식에 맞지 않게 입력하면 clean이 안뜬다.

또한 단순히 에러를 띄우는 것 외에도 데이터를 정리하는데에도 사용할 수 있다.
users - views.py

class LoginView(View):
    def get(self, request):
        form = forms.LoginForm(initial={"email": "123@123.com"})
        return render(request, "users/login.html", context={"form": form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        if form.is_valid():
            print(form.cleaned_data)
        return render(request, "users/login.html", context={"form": form})

cleaned_data는 필드를 정리해준 결과.

만약 clean_password를 지운다면?
users - forms.py

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_email(self):
        print("clean email")

    # def clean_password(self):
    #     print("clean password")

비밀번호가 none이 아니라 그대로 뜬다. 고로 clean_~~ 함수의 역할은 해당 요소를 확인하고 return을 하는 것이다.

self.cleaned_data를 해보면 유저가 입력한걸 그대로 가져온다. return도 입력한다면 우리가 원하는 정보를 return할 수 있다.

    def clean_email(self):
        print(self.cleaned_data)


유저가 이메일을 입력했을 때 db에서 사용자 정보와 비교해보고 맞으면 이메일을 반환, 틀리면 에러를 보여주자.
users - forms.py

from django import forms
from . import models


class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean_email(self):
        email = self.cleaned_data.get("email")
        try:
            models.User.objects.get(username=email)
            return email
        except models.User.DoesNotExist:
            raise forms.ValidationError("User does not exist")

    # def clean_password(self):
    #     print("clean password")

아무 이메일이나 입력했더니 없는 사용자라고 한다.

여기서 보면 views.py에 있는 print 구문이 출력이 안되었는데 그 위에 있는 form.is_valid()결과 유효한 이메일이 아니었어서 그렇다.

이제 비밀번호로 넘어가보자.
email은 해당 email만 확인하면 되지만 비밀번호는 email이 사용자의 것인지를 확인해야 하기에 불러와야 한다.
users - forms.py

    def clean_password(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return email
            else:
                raise forms.ValidationError("Password is wrong")
        except models.User.DoesNotExist:
            pass

clean_email도 있고 clean_password도 있고. 서로 다른 field가 서로 관련이 있으니 이를 확인하는 method를 만들자. 먼저 있던 clean_email을 지우고 통합하자.
users - forms.py

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(username=email)
            if user.check_password(password):
                return password
            else:
                raise forms.ValidationError("Password is wrong")
        except models.User.DoesNotExist:
            raise forms.ValidationError("User does not exist")

여전히 잘 뜬다. 근데 있는 이메일을 적어도 계속 User가 없다고 뜬다.


이유는 try에서 user 정보를 가져올 때 email아 아니라 username으로 email을 가져오게 했기 때문이다.
users - forms.py

        try:
            user = models.User.objects.get(email=email)

이제 문구가 바뀐 것을 볼 수 있다.

근데 에러가 비밀번호칸이 아니라 그냥 일반 에러처럼 떠있다. (에러가 nonfield로 되어있다)

코드를 좀 고쳐서 에러가 비밀번호칸에 뜨도록 하자.
users - forms.py

class LoginForm(forms.Form):

    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

    def clean(self):
        email = self.cleaned_data.get("email")
        password = self.cleaned_data.get("password")
        try:
            user = models.User.objects.get(email=email)
            if user.check_password(password):
                return password
            else:
                self.add_error("password", forms.ValidationError("Password is wrong"))
        except models.User.DoesNotExist:
            self.add_error("email", forms.ValidationError("User does not exist"))


이처럼 clean()을 사용시에는 한 field에 직접 에러를 추가해주어야한다.

만약 정상적으로 로그인을 한다면 views.py에서 form.is_valid()를 통과하며 cleaned_data가 출력된다.
(form은 해당 html 코드가 출력되는데 이걸 정제한 내용이 나온다.


유저의 비밀번호가 맞다면 cleaned_data를 반환하도록 하자.
users - forms.py - LoginForm

        try:
            user = models.User.objects.get(email=email)
            if user.check_password(password):
                return self.cleaned_data


이제 뭐가 유효한지 알 수 있다. clean()을 썼다면 cleaned_data를 반환하게 하도록 하자.

3) Login 구현

유저 인증 과정을 진행하자. authenticate 함수를 이용할 것이다.

로그인으로 인증을 시도하는데 이 함수는 인자로 username을 받으므로 로그인을 시도할 아이디를 이메일로 바꿔주자.

그리고 아이디와 암호가 맞다면 메인 페이지로 돌려보내도록 redirect와 reverse 함수를 써주자.
https://docs.djangoproject.com/en/4.0/ref/urlresolvers/#reverse

users - views.py

from django.views import View
from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth import authenticate, login, logout
from . import forms


class LoginView(View):
    def get(self, request):
        form = forms.LoginForm(initial={"email": "123@123.com"})
        return render(request, "users/login.html", context={"form": form})

    def post(self, request):
        form = forms.LoginForm(request.POST)
        if form.is_valid():
            email = form.cleaned_data.get("email")
            password = form.cleaned_data.get("password")
            user = authenticate(request, username=email, password=password)
            if user is not None:
                login(request, user)
                return redirect(reverse("core:home"))
        return render(request, "users/login.html", context={"form": form})

이제 로그인이 성공하면 메인으로 돌아온다. 참고로 메인에서 로그인을 하면 관리자 페이지도 자동으로 로그인된다. 하지만 로그인을 해도 메인에는 여전히 Login으로 떠있다.

user.is_authenticated 를 써보자.


templates - partials - nav.html

<a href="{% url 'core:home' %}">Hairbnb</a>
<ul>

    {% if user.is_authenticated %}
        <li><a href="{% url 'users:login' %}">Log out</a></li>
    {% else %}
        <li><a href="{% url 'users:login' %}">Login</a></li>
    {% endif %}
  
</ul>

여기서 nav.html이 어떻게 user에 접근하는지는 context processor라는게 해준다. request 객체를 인자로 받아 딕셔너리 형태로 반환하여 context에 병합되게 한다.

위 내용은 config - settings.py에도 명시되어있다.

...

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [os.path.join(BASE_DIR, "templates")],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
...

nav.html에서 작동하는 것은 쿠키를 가져와서 user를 찾고 그걸 template에 자동으로 넣어주는 것이다. 주로 딕셔너리 형태를 반환한다.
settings에 나와있는 context processors들을 차례대로 살펴보면 아래와 같다.
django.template.context_processors.debug

def debug(request):
    """
    Return context variables helpful for debugging.
    """
    context_extras = {}
    if settings.DEBUG and request.META.get("REMOTE_ADDR") in settings.INTERNAL_IPS:
        context_extras["debug"] = True
        from django.db import connections

        # Return a lazy reference that computes connection.queries on access,
        # to ensure it contains queries triggered after this function runs.
        context_extras["sql_queries"] = lazy(
            lambda: list(
                itertools.chain.from_iterable(
                    connections[x].queries for x in connections
                )
            ),
            list,
        )
    return context_extras

django.template.context_processors.request

def request(request):
    return {"request": request}

자 이제 여기서 user를 반환한다.
django.contrib.auth.context_processors.auth

def auth(request):
    """
    Return context variables required by apps that use Django's authentication
    system.

    If there is no 'user' attribute in the request, use AnonymousUser (from
    django.contrib.auth).
    """
    if hasattr(request, "user"):
        user = request.user
    else:
        from django.contrib.auth.models import AnonymousUser

        user = AnonymousUser()

    return {
        "user": user,
        "perms": PermWrapper(user),
    }

django.contrib.messages.context_processors

def messages(request):
    """
    Return a lazy 'messages' context variable as well as
    'DEFAULT_MESSAGE_LEVELS'.
    """
    return {
        "messages": get_messages(request),
        "DEFAULT_MESSAGE_LEVELS": DEFAULT_LEVELS,
    }

자 이제 로그인하면 Logout으로 뜬다.

로그아웃 기능도 넣어준다.
templates - partials - nav.html

<a href="{% url 'core:home' %}">Hairbnb</a>
<ul>

    {% if user.is_authenticated %}
        <li><a href="{% url 'users:logout' %}">Log out</a></li>
    {% else %}
        <li><a href="{% url 'users:login' %}">Login</a></li>
    {% endif %}
</ul>

users - views.py

def log_out(request):
    logout(request)
    return redirect(reverse("core:home"))

users - urls.py

urlpatterns = [
    path("login", views.LoginView.as_view(), name="login"),
    path("logout", views.log_out, name="logout"),
]

이제 로그아웃 기능이 잘 작동한다. admin에서도 같이 로그아웃이 된다.

4) LoginView

FormView에서 다음의 요소들을 사용할 것이다.
template_name, success_url, form_class, initial 등

users - views.py

class LoginView(FormView):

    template_name = "users/login.html"
    form_class = forms.LoginForm
    success_url = reverse("core:home")

이 상태에서는 다음의 에러가 콘솔에 뜬다. 어떤 패턴도 있지 않다고 한다.

따라서 reverse말고 reverse_lazy를 쓸 것이다. 기능은 동일하나 자동으로 호출하지 않는 것이다. View가 필요할 때만 호출한다.
로그인 화면은 뜬다.

form_valid를 추가하자.

users - views.py

class LoginView(FormView):

    template_name = "users/login.html"
    form_class = forms.LoginForm
    success_url = reverse_lazy("core:home")

    def form_valid(self, form):
        email = form.cleaned_data.get("email")
        password = form.cleaned_data.get("password")
        user = authenticate(self.request, username=email, password=password)
        if user is not None:
            login(self.request, user)
        return super().form_valid(form)

form_valid에서 통과가 되면 success_url로 넘어간다.
해보면 잘 된다.

에러도 잘 뜬다

profile
맨땅에 헤딩. 인생은 실전.

0개의 댓글