티스토리 뷰
[django] auth 앱을 통한 로그인 / 회원가입 및 email 전송 / 암호 변경 / session 및 쿠키
sikaro 2024. 5. 23. 16:43장고 auth 앱
https://github.com/django/django/tree/main/django/contrib/auth
회원가입 및 로그인 커스텀 페이지를 아예 앱으로 만들어놓은 게 있다.
User 모델 사용
user 모델을 사용하려면 수정해줘야 한다.
from django.contrib.auth.models import User
User.objects.all()
from django.contrib.auth import get_user_model
User = get_user_model()
author = models.ForeignKey('auth.User')
from django.conf import settings
author = models.ForeignKey(settings.AUTH_USER_MODEL)
from django.conf import settings
author = models.ForeignKey(setting.AUTH_USER_MODEL)
가장 마지막 방법이 추천하는 방법이다.
내가 별도로 변수 값을 읽어다가 사용하고 싶기 때문에, AUTH_USER_MODEL 값만 setting.py에서 바꾸면 된다.
User 모델 변경
- custom user 모델을 생성한다.
- django.contrib.auth.models.AbstractUser 상속
- Custom User 모델 설정 : '앱이름.모델명'
- settings.AUTH_USER_MODEL 값으로 지정한다.(기본값 : 'auth.User')
뷰에서 User 사용
FBV : request.user - 로그인하면 유저 인스턴스, 로그인하지 않으면 ananimous 인스턴스가 들어가 있다.
CBV : self.request.user
템플릿 : context_processors를 통해 user 제공 - 현재 사용자에 대한 정보가 필요하다.
User 타입
로그인 상태 : settings.AUTH_USER_MODEL
로그아웃 상태 : django.contirb.auth.models.AnonymousUser
layout.html에서 User를 사용하려면,
<body>
<nav class="navbar navbar-default">
<div class="container">
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active"><a href="/blog/">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{% if not user.is_authenticated %}
<li><a href="#">회원 가입 </a> </li>
<li><a href="#">로그인</a></li>
{% else %}
<li><a href="">{{user}}님!</a> </li>
<li><a href="/blog/profile/">내정보</a> </li>
<li><a href="#">로그 아웃</a></li>
{% endif %}
</ul>
</div>
</div>
</nav>
{{user}}를 쓰면 들어간다.
user.is_authenticated는 ananimous 일 경우에는 true이기 때문에, 해당으로 not이나 아니냐를 판단한다.
context_processors 등록
mysite/settings.py에 있는 template에 이미 등록되어 있다.
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
위에 있는 user.is_authenticated가 확인해준다.
뷰에서 사용한 객체를 template에서 쓸 수 있도록 넘겨주는 게 context이다.
응답이 나갈 때 얘네들이 실행된다.
ctrl+auth를 클릭하면 auth 함수로 이동하는데, return 함수가 user와 permwrapper(user)를 리턴한다.
request.user에 user 인스턴스가 들어 있다.
만약에 없다면 anonymoususer를 넣어준다.
로그인 / 로그아웃
accounts 앱 생성
장고쉘에서 accounts 앱을 만들어준다.
mysite> python manage.py startapp accounts
setting py에 앱 등록도 해준다.
https://github.com/django/django/blob/main/django/contrib/auth/urls.py
auth 앱의 기본적인 urls 양식이 있다.
# The views used below are normally mapped in the AdminSite instance.
# This URLs file is used to provide a reliable view deployment for test purposes.
# It is also provided as a convenience to those who want to deploy these URLs
# elsewhere.
from django.contrib.auth import views
from django.urls import path
urlpatterns = [
path("login/", views.LoginView.as_view(), name="login"),
path("logout/", views.LogoutView.as_view(), name="logout"),
path(
"password_change/", views.PasswordChangeView.as_view(), name="password_change"
),
path(
"password_change/done/",
views.PasswordChangeDoneView.as_view(),
name="password_change_done",
),
path("password_reset/", views.PasswordResetView.as_view(), name="password_reset"),
path(
"password_reset/done/",
views.PasswordResetDoneView.as_view(),
name="password_reset_done",
),
path(
"reset/<uidb64>/<token>/",
views.PasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
path(
"reset/done/",
views.PasswordResetCompleteView.as_view(),
name="password_reset_complete",
),
]
해당하는 걸 참고해서, accounts url을 등록해준다.
mysite/urls.py
앱이 추가되었으므로 추가
path("accounts/", include('accounts.urls')),
accounts/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
urlpatterns = [
path("login/", auth_views.LoginView.as_view(), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
]
일단 기본적으로 로그인과 로그아웃만 추가해준다.
그리고 accounts 앱에서 사용하는 환경변수 값을 변경해준다.
mysite/settings.py
LOGIN_REDIRECT_URL = '/blog/'
LOGOUT_REDIRECT_URL = '/blog/'
로그인 페이지도 만들어준다.
이때 폴더는 templates/registration/login.html로 한다.
{% extends "layout.html" %}
{% block content %}
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table}}
</table>
<input type="submit" />
</form>
{% endblock %}
해당 로그인과 로그아웃에 대해, url 'login'을 지정하면, 위에서 accounts/urls.py에서 name으로 지정한 url의 view가 실행된다.
<ul class="nav navbar-nav navbar-right">
{% if not user.is_authenticated %}
<li><a href="#">회원 가입 </a></li>
<li><a href="{% url 'login' %}?next={{request.path}}">로그인</a></li>
{% else %}
<li><a href="">{{user}}님!</a></li>
<li><a href="/blog/profile/">내정보</a></li>
<li><a href="{% url 'logout' %}?next={{request.path}}">로그 아웃</a></li>
{% endif %}
로그인 / 로그아웃 작업 후 이전화면으로 이동하려면 request.path를 넣어준다.
작업하던 그 경로로 돌아간다.
회원 가입
회원 가입 form도 django auth에 이미 있다.
가져다 쓰면 된다.
다만 view는 만들어야 한다.
accounts/templates/registration/signup.html
{% extends "layout.html" %}
{% block title %}
회원가입
{% endblock %}
{% block content %}
<h1> 회원 가입 </h1>
<form action="" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" />
</form>
{% endblock %}
회원 가입 폼을 만들고, 회원가입 뷰를 작성한다.
accounts/views.py
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.conf import settings
def signup(request):
if request.method == 'POST':
form = UserCreationForm(request.POST)
if form.is_valid():
form.save()
return redirect(settings.LOGIN_URL)
else:
form = UserCreationForm()
return render(request, 'registration/signup.html',{'form':form})
post면 signupform에서 확인하고, form을 save한다.
아니라면 signupform 페이지를 연다.
해당에서 login url은 setting에 지정해줘야 한다.
mysite/settings.py
LOGIN_URL = '/accounts/login/'
회원가입 url도 지정해준다.
accounts/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
urlpatterns = [
path("login/", auth_views.LoginView.as_view(), name="login"),
path("logout/", auth_views.LogoutView.as_view(), name="logout"),
path('signup/', views.signup, name='signup'),
]
한가지 주의할 점은, auth_views와 views가 다르다는 점.
저 views는 자신이 직접 지정한 view기 때문에 직접 불러와줘야 한다.
당연히 회원 가입 페이지도 url에 등록해줬기 때문에, layout.html에 등록해줘야 클릭해서 들어가진다.
<li><a href="{% url 'signup' %}">회원 가입 </a></li>
accounts/forms.py를 이용해서 회원가입을 해준다.
from django.contrib.auth.forms import UserCreationForm
class SignupForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
fields = UserCreationForm.Meta.fields + ('email', )
UsercreationForm 상속
signup form을 상속받아서, signup을 고쳐준다.
from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.conf import settings
from .forms import SignupForm
def signup(request):
if request.method == 'POST':
form = SignupForm(request.POST)
if form.is_valid():
form.save()
return redirect(settings.LOGIN_URL)
else:
form = SignupForm()
return render(request, 'registration/signup.html',{'form':form})
이러면 이제 회원가입에서 email이 들어갈 수 있다.
accounts/models.py에서, user와 1:1 관계를 가지는 profile 모델을 정의한다.
from django.db import models
from django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=20)
address = models.CharField(max_length=50)
그 후에는 migrations를 해줘야 한다.
python manage.py makemigrations accounts
python manage.py migrate accounts
최종 signupform은 다음과 같다.
accounts/forms.py
from django.contrib.auth.forms import UserCreationForm
from django import forms
from .models import Profile
class SignupForm(UserCreationForm):
phone_number = forms.CharField()
address = forms.CharField()
class Meta(UserCreationForm.Meta):
fields = UserCreationForm.Meta.fields + ('email', )
def save(self):
user = super().save()
Profile.objects.create(user=user,
phone_number=self.cleaned_data['phone_number'],
address=self.cleaned_data['address'])
return user
이제 회원가입을 하면, 데이터베이스에 추가가 되는 걸 볼 수 있다.
https://docs.djangoproject.com/en/4.2/topics/email/
django에서 이메일은 해당 라이브러리로 쓴다.
global_settings.py에 이미 들어가 있다.
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
Email Naver SMTP 연동
Backend에다가 smtp.emailbackend해서 네이버 이멜, 비밀번호만 주고 send_mail만 해주면 이메일 전송을 해준다.
네이버 메일에서 pop3를 활용해야 한다.
mysite/settings.py
EMAIL_BACKEND= 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST= 'smtp.naver.com'
EMAIL_PORT= 465
EMAIL_HOST_USER= 이메일
EMAIL_HOST_PASSWORD= 비밀번호
EMAIL_USE_SSL= True
이런 거 깃허브에 올릴때는 조심해야 한다.
해커들이 실시간으로 수집하는 중
Console에서 Email 전송 Test하기
mysite/settings.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
한 후에, 장고쉘에서
from django.core.mail import send_mail
하고, send_mail 함수로
send_mail('hello', 'i miss you' , 'admin@sky.com', ['amy@gmail.com'], fail_silently=False)
하면, 실제 이메일이 날아간다.
Signals
이메일을 보내는 건 알았는데, 실제 이메일을 보낼 때는 어떠한 회원이 회원가입한 때이다.
회원가입한 때 보낼 수 있게 하는 게 바로 signal이다.
https://docs.djangoproject.com/en/4.2/ref/signals/
Model signals
인스턴스를 생성할 때 내부적으로 생성자 호출
pre_init
post_init - init 메소드가 끝남
pre_save - save 메소드 실행되기 전
post_save - save 메소드 실행된 후
pre_delete
post_delete
m2m_changed
class_prepared
Managemnet signals
pre_migrate
post_migrate
Request/response signals
request_started
request_finished
got_request_exception
Test signals
setting_changed
template_rendered
회원가입이 완료된 후에 쓰고 싶으면, post_save 후에 하는 게 맞다.
이메일 전송해주는 함수를 어디선가 하나 만들어놓고, post_save가 될 때 해당 함수를 실행시켜달라고 하면 된다.
signal 클래스에 connect라는 메소드가 있는데,
실행할 함수하고 연결해준다.
receiver = 해당 시그널을 감지했을 때 실행할 함수
post_save
시그널마다 이벤트가 발생했을 때 보내주는 신호가 다르다.
post_save가 될 때 동작하면서 넘어가는 argument들이 있는데, argument들은 다음과 같다.
sender - 모델의 클래스 정보.
instance - 이메일 값이나 그런 입력된 정보들
created - 만약에 생성되었다면 True
회원가입 후 환영 Email 보내기
accounts/models.py
from django.db import models
from django.conf import settings
from django.core.mail import send_mail
from django.db.models.signals import post_save
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
phone_number = models.CharField(max_length=20)
address = models.CharField(max_length=50)
def on_send_mail(sender, **kwargs):
if kwargs['created'] :
user = kwargs['instance']
send_mail('가입인사','가입을 환영합니다.', 'admin@sky.com', [user.email], fail_silently=False)
post_save.connect(on_send_mail, sender=settings.AUTH_USER_MODEL)
회원가입을 하고, 콘솔을 확인해보면 된다.
Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit
Subject: =?utf-8?b?6rCA7J6F7J247IKs?=
From: admin@sky.com
To: kkkk@gmail.com
Date: Thu, 23 May 2024 05:51:25 -0000
Message-ID: <171644348510.1672.3743033087074139615@DESKTOP-F1O3Q8D>
가입을 환영합니다.
이 과정은 mysite/settings.py에서 emailbackend가 설정되어 있어야 작동한다.
Messages
딱 한번 보여주는 메세지이다.
Csrfmiddelware가 이 메세지와 같다.
MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
]
메세지 미들웨어가 있다.
MiddlewareMixin을 상속받는다.
만약에 미들웨어를 만들고 싶으면 해당 클래스를 상속받으면 된다.
view가 실행되기 전, 클라이언트가 응답을 나갈 때 작동한다.
그 중간에 실행되는 건 process_request이다.
암호 변경
accounts/urls.py를 다음과 같이 고쳐준다.
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views
from django.views.generic import TemplateView
urlpatterns = [
path('login/', auth_views.LoginView.as_view(), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('signup/', views.signup, name='signup'),
path(
"password_change/", views.MyPasswordChangeView.as_view(), name="password_change"
),
path(
"password_change/done/",
auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done",
),
path('profile/', TemplateView.as_view(template_name='registration/profile.html'), name='profile' ),
path('cookie/<code>/', views.cookie_test),
path('session/<int:code>', views.session_test),
]
success_url = password_change_done이 reverse_lazy안에 들어 있다.
해당 설정은 auth앱에 있는 passwordchangeview에 들어 있다.
PasswordChangeDoneView.as_view()로 완료 되었다는 걸 불러온다.
만약 싫다면 succes_url을 바꾸면 된다.
내정보 페이지 - profile.html을 만든다.
{% extends "layout.html" %}
{% block content %}
<h1> 내정보 </h1>
<ul>
<li> name : {{user}}</li>
<li> email : {{user.email}}</li>
<li> tel : {{user.profile.phone_number}}</li>
<li> address : {{user.profile.address}}</li>
<li>{{user.date_joined}}</li>
</ul>
<a href="{% url 'password_change' %}">[암호 변경하기]</a>
{% endblock %}
layout에 로그인했을 때, 자기 profile이 보이게도 해준다.
<li><a href="{% url 'profile' %}">내정보</a></li>
해당 url 링크는 단순히 내정보 창으로 가게 한다.
그러나 암호 변경하기는 안에서 django의 공식 암호 변경 페이지로 이동해서, 암호를 변경해준다.
message 추가
views.py에 message를 보내는 기능을 추가해준다.
class MyPasswordChangeView(PasswordChangeView) :
success_url = reverse_lazy('profile')
def form_valid(self, form):
messages.info(self.request, '암호 변경을 완료했습니다!')
return super().form_valid(form)
메세지를 info라는 함수로 만들면, 그냥 하나의 메세지를 출력해주는 코드가 된다.
contenxt processer에서 messages라는 context가 있다.
messages라는 이름으로 모든 메세지를 가지고 있는 객체가 있다.
그 message를 get 해서 뿌려준다.
layout.html에서 messages도 등록해준다.
<div class="container">
<div class="row">
<div class="col-sm-12">
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message}}</li>
{% endfor %}
</ul>
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
</div>
여기까지 하면, 암호 변경을 완료 했을 때, 내정보의 화면에 암호 변경 메시지가 뜬다.
쿠키
쿠키는 request에 있는 쿠키 파일에서 쓴다.
accounts/views.py
def cookie_test(request, code):
response = render(request, 'registration/cookieTest.html')
if code == 'add':
response.set_cookie('model', 'A001')
response.set_cookie('prod', 'EV9')
elif code == 'get' :
model = request.COOKIES.get('model')
prod = request.COOKIES.get('prod')
print(model, prod)
elif code == 'del' :
response.delete_cookie('model')
response.delete_cookie('prod')
return response
쿠키 이름은 알아서 지정해주면 된다.
urls.py에 해당 path를 추가해주면, 쿠키를 볼 수 있다.
path('cookie/<code>/', views.cookie_test),
cookieTest를 위한 html을 만든다.
{% extends "layout.html" %}
{% block content %}
<h1> 저장된 Cookie </h1>
<h2 style="color:blue">
{{request.COOKIES}}
</h2>
{% endblock %}
그러면, http://127.0.0.1:8000/accounts/cookie/add/
http://127.0.0.1:8000/accounts/cookie/get/
http://127.0.0.1:8000/accounts/cookie/del/
후에 다시
http://127.0.0.1:8000/accounts/cookie/get/
에서 결과를 볼 수 있다.
Session
http 프로토콜은 무상태(stateless)라는 특성을 가지고 있다.
한번 요청하고 응답하면 연결이 끊긴다.
요청-응답 단위로 매겨진다.
하나의 커넥션에서 썼던 데이터를 다른 커넥션에서 쓸 수 없다.
데이터가 일회용이다.
원하는 데이터를 유지하고 싶다면, 이 데이터를 어딘가에 저장해놔야 하는데, 그게 클라이언트 쪽이면 쿠키이고, 서버 쪽이면 세션이다.
중복 로그인이 되어 있으면 안된다.
해당하는 것을 위해서 cotrib/sessions/backend/db.py를 사용한다.
세션 test view code
accounts/views.py
from django.contrib.sessions.backends.db import SessionStore
# Session Test
def session_test(request, code):
response = render(request, 'registration/sessionTest.html')
session = request.session
if code == 1:
user = request.user
print(user,':', session)
elif code == 2 :
session['model'] = 'A001'
session['prod'] = 'EV9'
print('session 데이터 등록')
elif code == 3:
model = session.get('model')
prod = session.get('prod')
print('session 데이터 추출')
print(model, prod)
elif code == 4:
session.pop('model')
print('session 데이터 삭제')
session.pop('prod')
return response
sessionTest.html도 만들어주고, accounts/urls.py에도 path를 추가해준다.
path('session/<int:code>', views.session_test),
{% extends "layout.html" %}
{% block content %}
<h1> Session Test </h1>
<h2 style="color:green">
{{request.user}}
</h2>
<h2 style="color:red">
{{request.session.session_key}}
</h2>
{% endblock %}
이제 해당하는 창으로 가서 테스트한다.
http://127.0.0.1:8000/accounts/session/1 - 로그아웃상태
http://127.0.0.1:8000/accounts/session/1 - 로그인 상태
1~4로 데이터 등록과 삭제를 할 수 있다.
중복 로그인 방지
세션 ID 값을 할당되었을 때, 같은 세션이 할당되었다면 해당 세션이 중복이 되는지를 확인하고 생성한다.
models.py에 유저 세션을 저장하기 위한 테이블을 추가한다.
accounts/models.py
class UserSession(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
session_key = models.CharField(max_length=40)
created_at = models.DateTimeField(auto_now_add=True)
def block_duplicate_login(sender, request, user, **kwargs):
login_user_list = UserSession.objects.filter(user=user)
for user_session in login_user_list:
session = SessionStore(user_session.session_key)
session.delete()
# session['blocked'] = True
# session.save()
session_key = request.session.session_key
UserSession.objects.create(user=user, session_key = session_key)
user_logged_in.connect(block_duplicate_login)
model이 만들어졌으므로 migration은 습관처럼
python manage.py makemigrations accounts
python manage.py migrate accounts
두 개의 창을 열면, 아직까지는 로그인이 된다.
이제는 delete를 해주지 않고, 서버에 저장해서 해당하는걸 가져온다.
models.py
def block_duplicate_login(sender, request, user, **kwargs):
login_user_list = UserSession.objects.filter(user=user)
for user_session in login_user_list:
session = SessionStore(user_session.session_key)
# session.delete()
session['blocked'] = True
session.save()
session_key = request.session.session_key
UserSession.objects.create(user=user, session_key = session_key)
user_logged_in.connect(block_duplicate_login)
그리고 중복 실행시 메시지를 보이기 위한 미들웨어 파일을 추가해준다.
accounts//middleware.py
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from django.contrib import messages
from django.contrib.auth import logout as auth_logout
from django.shortcuts import redirect
class BlockedMiddleware(MiddlewareMixin):
def process_request(self, request):
blocked = request.session.pop('blocked', None)
if blocked:
messages.info(request,'다른 기기에서 동일아이디로 로그인되어 자동으로 로그아웃 되었습니다.')
auth_logout(request)
return redirect(settings.LOGIN_URL)
middleware가 만들어졌으니, settings.py에서 middleware란에도 추가해준다.
"accounts.middleware.BlockedMiddleware"
이러면 두 개의 창에서 차례대로 로그인 작업을 했을 시에, 처음 로그인창이 로그아웃되며 활성화된다.