로그인 기능을 구현할 것이다.
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
짠
로그인시 장고는 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를 반환하게 하도록 하자.
유저 인증 과정을 진행하자. 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에서도 같이 로그아웃이 된다.
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로 넘어간다.
해보면 잘 된다.
에러도 잘 뜬다