이번 블로깅은 USER LOG IN & LOG OUT
에 대해 다루어보고 이에 필요한 CSRF
까지 알아보겠습니다.
# 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
views에 View를 import하고 Class기반으로 기능들을 작성합니다.
# users/urls.py
from django.urls import path
from . import views
app_name = "users"
urlpatterns = [path("login", views.LoginView.as_view(), name="login")]
app_name이 user가 되도록 app의 url을 연결합니다.
# config/urls.py
urlpatterns = [
path("users/", include("users.urls", namespace="users")),
...
]
config 프로젝트 설정파일에도 url을 연결합니다.
forms.py에는 Login을 위한 Form이 위치합니다. form에는 Login을 위한 form이 들어있고, email과 password를 입력해주었습니다.
# users/forms.py
from django import forms
class LoginForm(forms.Form):
email = forms.EmailField()
password = forms.CharField(widget=forms.PasswordInput)
email이 user의 아이디가 되도록 코드를 구성했습니다. 이것으로 user가 중복되는지 체크하고 email도 체크하는 2번의 작업을 1번으로 줄일 수 있습니다.
widget=forms.PasswordInput
으로 user가 비밀번호를 입력할 때 비밀번호가 가려질 수 있도록 widget을 설정 합니다.
# users/views.py
from django.views import View
from django.shortcuts import render
from . import forms
class LoginView(View):
def get(self, request):
return render(request, "users/login.html")
form = forms.LoginForm()
return render(request, "users/login.html", {"form": form})
def post(self, request):
form = forms.LoginForm(request.POST)
print(form)
get
메서드에서는 LoginForm의 형식을 form으로 저장하고, form을 context안으로 보내주기 위해 {"form": form}
를 지정해주어 login창이 form을 가지도록 합니다.
프론트엔드 부분을 Template으로 채웠습니다.
{% block content %}
<form method="POST" action="{% url "users:login" %}">
{% csrf_token %}
{{form.as_p}}
<button>Login</button>
</form>
{% endblock content %}
login 버튼을 누르면 POST
할 수 있도록 form의 메서드를method="POST"
으로 지정하고, users:login
으로 url을 거치도록 합니다.
사이트 간 요청 위조 확인에 오류가 생기지 않도록 csrf_token
을 설정해줍니다.
csrf_token
사이트 간 요청 위조(Cross-site request forgery, CSRF)는 웹사이트 취약점 공격의 하나로, 사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹사이트에 요청하게 하는 공격을 말합니다.
예시로 설명을 풀어가겠습니다.
'페이스북' 웹사이트에 로그인을 할 때 웹사이트는 쿠키를 줍니다. 그래서 페이스북에 접속할 때마다 자동적으로 쿠키를 페이스북에 보내는데요. 문제는 사용자가 페이스북이 아닌 다른 웹사이트를 방문했을 때 생깁니다.
다른 웹사이트에, 예를 들어 인스타그램, 버튼이나 이상한 자바스크립트를 가지고 있고 그 버튼을 누르면 인스타그램한테가 아닌 페이스북한테 ajax 등을 사용해 무언가를 요청합니다.
이 때 그 요청은 사용자의 브라우저에서 일어났기 때문에 브라우저는 자동적으로 쿠키를 보내는데, 인스타그램에서 버튼을 누르면 페이스북 쿠키를 페이스북 백엔드쪽으로 보내며 이때 공격자가 의도한 행위(비밀번호를 어떻게 바꿀지 등..)대로 공격합니다.
그래서 악의적인 링크 등을 눌러도 사용자의 웹사이트가 방어(대응)을 하도록 설계되어있는데, 장고는 대응의 방법으로{%csrf_token %}
을 사용합니다.
공격자들이 아무리 post request
를 보낸 것을 찾았다 할지라도 token의 번호가 다르기 때문에 공격을 대응합니다.
입력된 데이터가 유효한지 확인하기 위해 2가지를 수행합니다. 첫 번째는 is_valid
라는 function을 사용합니다.
# 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(self):
email = self.cleaned_data.get("email")
password = self.cleaned_data.get("password")
try:
user = models.User.objects.get(email=email) # email이 사용자의 username
if user.check_password(password):
return self.cleaned_data
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
라는 메서드를 만들어줍니다. (항상 clean_"name"
으로 시작해야 합니다.) 그리고 예외처리로 username이 email과 같은 오브젝트가 있다면 email을 return하고 그렇지 않다면 ValidationError
를 raise합니다.
이때 유저도 존재하고 비밀번호도 맞다면 cleaned_data안에 저장하도록 합니다. 마지막으로 어떤 필드에서 에러가 왔는지를 알려주기 위해 "password"
혹은 "email"
의 값을 입력해주었습니다.
user.check_password(password)
중 check_password 메서드를 통해 주어진 string이 맞으면 True
를 return하고 그렇지 않으면 False
를 return합니다.
clean()을 사용한다면 해당 코드처럼 한 field에 직접 에러를 추가해야 하고, cleaned_data를 return해야 합니다.
# 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": "itn@las.com"})
return render(request, "users/login.html", {"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", {"form": form})
유저를 Login, Logout하기 위해선 두 가지 과정이 필요하며 이는 인증을 하고 로그인을 시키는 것입니다.
우선 인증은 username
과 password
를 필요로 합니다. 그 후에 user를 return합니다. 이 과정을 거치면 장고가 쿠키를 해주는 등 알아서 진행해줍니다.
post 메서드에 email과 password는 get을 통해 가져옵니다. 그리고 authenticate, login, logout를 import합니다.
# users/views.py
from django.views import View
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms
class LoginView(View):
def get(self, request):
form = forms.LoginForm(initial={"email": "davidkim@gmail.com"})
return render(request, "users/login.html", {"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", {"form": form})
def log_out(request):
logout(request)
return redirect(reverse("core:home"))
username=email
, password=password
인지 authenticate
를 한 후에 user가 있다면 login을 해주고 원래 있던 곳(core:home)으로 redirect(돌려 보냄)를 합니다. reverse
는 "core:home"으로 가서 실제 URL을 가져옵니다.
template에서 user가 인증되었으면 다음의 코드로 Login 영역을 더이상 보여주지 않도록 합니다.
{% if user.is_authenticated %}
<li><a href="{% url "users:logout" %}">Log out</a></li>
{% else %}
<li><a href="{% url "users:login" %}">Log in</a></li>
{% endif %}
users앱에 login, logout url을 연결은 필수!
# users/urls.py
urlpatterns = [
path("login", views.LoginView.as_view(), name="login"),
path("logout", views.log_out, name="logout"),
]
FormView를 사용하면 더 적은 코드로 구현할 수 있습니다. FormView에선 Post, Get을 사용하지 않고 구현할 수 있습니다. (FormView에선 template_name을 요구합니다.)
#users/views.py
from django.views import View
from django.views.generic import FormView
from django.urls import reverse_lazy
from django.shortcuts import render, redirect, reverse
from django.contrib.auth import authenticate, login, logout
from . import forms
class LoginView(FormView):
template_name = "users/login.html"
form_class = forms.LoginForm # LoginForm(initialize) 하지 않습니다.
success_url = reverse_lazy("core:home")
# reverse_lazy : 자동으로 호출하지 않고 View가 필요로 할 때 호출합니다.
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)
def log_out(request):
logout(request)
return redirect(reverse("core:home"))
classy class based view에서 더 많은 view들을 검색할 수 있으며, 추가적으로 여기서는 사용되지 않았지만 장고에는 authentication_form, PasswordChangeForm, PasswordResetForm, SetPasswordForm, UserChangeForm, UserCreationForm 등 다양한 Form이 있습니다.