- 기본 User 모델은 username 기반인데 email 기반으로 변경할 것임
- migrations 하기전에 커스터마이징을 해줘야함 했으면 db.sqlite3 지우고 동작
class User(AbstractBaseUser): # 기존에 있는것 말고 커스터마이징 할 것 작성
email = models.EmailField(verbose_name='email', unique=True)
is_active = models.BooleanField(default=False) # is actvie 가 활성화되어 있지 않으면 로그인이 안됨 email 이 인증되면 로그인 활성화하는 작업
is_admin = models.BooleanField(default=False) # admin 사이트에 들어갈수 있는지
nickname= models.CharField('nickname', max_length=20, unique=True)
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
from django.db import models
# Create your models here.
# 기존 Manager를 UserManager 로 대체하는 것이며 BaseUserManager로 부터 상속받기에 기존 기능은 다 사용 가능
# 이메일 기반으로 만들기 위해 오버라이드 해서 생성하는 것
class UserManager(BaseUserManager):
# 일반 사용자 생성, admin 페이지에서 생성
def create_user(self, email, password, nickname):
if not email:
raise ValueError('올바른 이메일을 입력하세요.')
user = self.model(
email = self.normalize_email(email), # 이메일 정규화
nickname = nickname
)
user.set_password(password) # set_password 해쉬화 진행
user.is_active = False
user.save()
return user
# 슈퍼 사용자 생성 , python manage.py createsuperuser
def create_superuser(self, email, password, nickname):
user = self.create_user(email, password, nickname)
user.is_admin = True # 슈퍼유저 권한 설정
user.is_active = True # 슈퍼유저는 기본적으로 활성화됨
user.save(using=self._db) # using=self._db -> 다중 DB 환경에서도 안전하게 저장
return user
# 암화화는 qwer1234 -> asdflskd32341 -> 복호화 -> qwer1234 -> 즉 암호화는 복호화가 가능함
# 해쉬화 qwer 1234 -> asdflas/k32341 -> 암호화(asdfas) -> 암호화를 반복 -> 뒷부분 -> 복호화가 불가능
# SHA256 (장고에서 사용하는 것)
class User(AbstractBaseUser): # 기존에 있는것 말고 커스터마이징 할 것 작성
email = models.EmailField(verbose_name='email', unique=True)
is_active = models.BooleanField(default=False) # is actvie 가 활성화되어 있지 않으면 로그인이 안됨 email 이 인증되면 로그인 활성화하는 작업
is_admin = models.BooleanField(default=False) # admin 사이트에 들어갈수 있는지
nickname= models.CharField('nickname', max_length=20, unique=True)
objects = UserManager() # ✅ 커스텀 매니저 연결
USERNAME_FIELD = 'email' # ✅ 기본 로그인 필드를 `email`로 설정
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = [] # ✅ `createsuperuser` 명령어 실행 시 필수 입력 필드 설정 (이메일만 필수)
class Meta:
verbose_name = '유저'
verbose_name_plural = f'{verbose_name} 목록'
# 아래기능은 재활용해서 쓰자 다음부턴
# ✔ Django가 사용자 객체를 다룰 때 필요한 기본 메서드를 구현
# ✔ has_perm() & has_module_perms() → 기본적으로 모든 권한을 부여하지만, 세부적으로 관리 가능
def get_full_name(self):
return self.nickname
def get_short_name(self):
return self.nickname
def __str__(self):
return self.nickname
def has_perm(self, perm, obj = None):
return True
def has_module_perms(self, app_label):
return True
@property
def is_staff(self):
return self.is_admin
@property
def is_superuser(self):
return self.is_admin
# @property 가 붙어있으면 함수 호출시 is_staff() 가아니라 is_staff 로 사용 가능
AUTH_USER_MODEL='member.User' # 이번 장고프로젝트에서 User는 내가 만든 User로 쓴다는 선언
# INSTALLED_APP 에 member 추가
poetry add django-extensions, ipython
-> django_extensions 인설트앱 추가python manage.py createsuperuser
-> email 로 만들어 지는 것 확인STATIC_DIR = BASE_DIR / 'static'
STATICFILES_DIRS = [
STATIC_DIR,
]
STATIC_ROOT = BASE_DIR / '.static_root'
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
User = get_user_model() # 우리가 입력한 Auth의 Usermodel 불러옴
class SignupForm(UserCreationForm):
class Meta(UserCreation.Meta):
model = User # 위에서 불러온 User 모델 사용
fields = ('email', 'password1', 'password2')
labels = {
'email' : '이메일',
'password1' : '비밀번호',
'password2' : '비밀번호 확인',
}
widgets ={
'email' : forms.EmailInput( # 이메일 형태의 인풋만 받게 하기 위해
attrs={
'placeholder' : 'example@example.com',
}
)
}
# 🚨 password1, password2의 위젯이 적용되지 않는 이유
# • password1, password2는 UserCreationForm에서 CharField + PasswordInput 위젯으로 이미 정의되어 있음.
# • Meta.widgets는 **모델 필드(models.py에 정의된 필드)**에만 적용되며,
# UserCreationForm에서 정의된 password1, password2는 모델 필드가 아니라 Form 필드이기 때문.
# 그래서 아래와 같이 작성해야함
from django import forms
from django.contrib.auth import get_user_model, authenticate
from django.contrib.auth.forms import UserCreationForm
User = get_user_model() # 이렇게 입력하면 우리가 입력해놓은 Auth의 Usermodel 을 가져옴
class SignupForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class_update_fields = ('password1','password2')
for field in class_update_fields:
# if field.startswith('password'):
# self.fields[field].label = '비밀번호'
self.fields[field].widget.attrs['class'] = 'form-control'
self.fields[field].widget.attrs['placeholder'] = 'password'
if field == 'password1':
self.fields[field].label = '비밀번호'
else:
self.fields[field].label = '비밀번호 확인'
class Meta(UserCreationForm.Meta):
model = User
fields = ('email', 'nickname')
labels = {
'email' : '이메일',
'nickname' :'닉네임',
}
widgets = {
'email' : forms.EmailInput(
attrs={
'placeholder': 'example@example.com',
'class' : 'form-control',
},
),
'nickname': forms.TextInput(
attrs={
'placeholder':'닉네임',
'class': 'form-control',
}
)
}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pystgram</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}">
</head>
<body>
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{% static 'js/bootstrap.bundle.js' %}"></script>
</body>
</html>
# member/views.py
from django.views.generic import FormView
from member.forms import SignupForm
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
# config/urls.py
from member import views as member_views
urlpatterns = [
path('admin/', admin.site.urls),
# auth
path('signup/', member_views.SignupView.as_view(), name = 'signup'),
]
<!-- singup.html -->
{% extends 'base.html' %}
{% block content %}
<div>
<h1 class="title">회원가입</h1>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button>회원가입</button>
</form>
</div>
{% endblock %}
<!-- signup.html -->
{% extends 'base.html' %}
{% block content %}
<div>
<h1 class="title">회원가입</h1>
<form method="POST">
{% csrf_token %}
{% for field in form %}
<div class="form-group row mb-2">
<label class="form-label col-md-2" for="{{ field.auto_id }}">{{ field.label }}</label>
<div class="col-md-10">
{{ field }}
</div>
<!-- 장고에서 필드에서 오류가 났을떄 오류 띄워줌 -->
{% if field.errors %}
{% for error in field.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
{% endfor %}
<!-- non_field_error 처리 -->
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% endif %}
<button class="btn btn-primary">회원가입</button>
</form>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block content %}
<div class="col-8 offset-2">
<h1>회원가입이 완료되었습니다.</h1>
<p>
이메일 인증을 하여 회원가입을 완료해주세요.
</p>
</div>
{% endblock %}
# config.urls.py
path('signup/done/', TemplateView.as_view(template_name='auth/signup_done.html'), name = 'signup_done'),
# members/views.py
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import FormView
from member.forms import SignupForm
# Create your views here.
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
success_url = reverse_lazy('signup_done') # success_url 은 reverse lazy
def form_valid(self, form):
form.save()
return HttpResponseRedirect(self.get_success_url())
# members/views.py
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
# success_url = reverse_lazy('signup_done') # success_url 은 reverse lazy
def form_valid(self, form):
user = form.save()
return render(self.request, # FBV 에서는 자체함수에서 request 를 가져오지만 상속받아온 self.request 쓰면됨
template_name='auth/signup_done.html',
context= {'user': user}
)
# config/urls.py 아래부분 주석처리
# path('signup/done/', TemplateView.as_view(template_name='auth/signup_done.html'), name = 'signup_done'),
- 프로젝트 내에서 만들어서 깃허브에 올리면 비밀번호 같은 것들이 유출될 우려가 있다.
- 그래서 컴퓨터 내에 폴더를 만들어 관리하는 것을 환경변수라 한다.
- 일반적으로 dot.env 로 관리하지만 다른 방식으로 한번 해보기
{
"DB": {
"HOST" : "ABCD",
"USER" : "SAD",
"PASSWORD": "Asss"
},
"DB2": {
"HOST" : "ABCD2",
"USER" : "SAD2",
"PASSWORD": "Asss2"
}
}
# config/settings.py BASE_DIR 밑에 작성
with open(BASE_DIR / '.config_secret' / 'secret.json') as f :
config_secret_str = f.read()
SECRET = json.loads(config_secret_str)
# 제일밑에
print(SECRET['DB']['HOST']) # runserver시 호스트가 잘나오나 확인
{
"DJANGO_SECRET_KEY" : "django-insecure--_o1uke5zi&9+on4o(fr-$5k83uam2f14@gdeu(6)e32qqvhv$"
}
SECRET_KEY = SECRET['DJANGO_SECRET_KEY']
{
"DJANGO_SECRET_KEY": "django-insecure--_o1uke5zi&9+on4o(fr-$5k83uam2f14@gdeu(6)e32qqvhv$",
"email": {
"user": "본인 이메일",
"password" : "PXB1Q889R1LY" # 앱비밀번호
}
}
# email
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.naver.com'
EMAIL_USE_TLS = True # 보안연결
EMAIL_PORT = 587
EMAIL_HOST_USER = SECRET["email"]["user"]
EMAIL_HOST_PASSWORD = SECRET["email"]["password"]
In [1]: from django.core.mail import send_mail
In [2]: title='제목입니다.'
In [3]: message='이것은 장고에서 보낸 메세지입니다.'
In [4]: from_email = settings.EMAIL_HOST_USER
In [5]: to_email=['gkr054@naver.com']
In [6]: send_mail(title,message,from_email,to_email)
Out[6]: 1
In [7]: title='제목입니다.2'
In [8]: send_mail(title,message,from_email,to_email)
Out[8]: 1
class VerifyEmailView(APIView):
def get(self,request):
token= request.GET.get("token")
if not token:
return Response({"error": "토큰이 없습니다."}, status=status.HTTP_400_BAD_REQUEST)
try:
decode = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user = User.objects.get(id=decode["user_id"])
if user.email_verified:
return Response({"message": "이미 인증된 계정입니다."}, status=status.HTTP_400_BAD_REQUEST)
user.email_verified=True
user.is_active=True
user.save()
return Response({"message": "이메일 인증이 완료되었습니다. 계정이 인증되었습니다."}, status.HTTP_200_OK)
except jwt.ExpiredSignatureError:
return Response({"error": "토큰이 만료되었습니다."}, status=status.HTTP_400_BAD_REQUEST)
except jwt.DecodeError:
return Response({"error": "잘못된 토큰입니다."}, status=status.HTTP_400_BAD_REQUEST)
except User.DoesNotExist:
return Response({"error": "존재하지 않는 사용자입니다."}, status=status.HTTP_404_NOT_FOUND)
class UserLoginView(APIView):
def post(self, request):
email = request.data.get("email")
password = request.data.get("password")
user = authenticate(email=email, password=password)
if not user:
return Response({"error":"이메일 또는 비밀번호가 올바르지 않습니다."}, status=status.HTTP_400_BAD_REQUEST)
if not user.email_verified:
return Response({"error": "이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요."}, status=status.HTTP_403_FORBIDDEN)
refresh = RefreshToken.for_user(user)
access_token = str(refresh.access_token)
store_access_token(user.id, access_token, 3600)
return Response({
"access_token": access_token,
"refresh_token": str(refresh),
"token_type": "Bearer",
"expires_in": 3600
}, status=status.HTTP_200_OK)
from django.conf import settings
from django.core import signing
from django.core.signing import TimestampSigner, SignatureExpired
from django.shortcuts import render, get_object_or_404
from django.views.generic import FormView
from member.forms import SignupForm
from utils.email import send_email
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
# success_url = reverse_lazy('signup_done') # success_url 은 reverse lazy
def form_valid(self, form):
user = form.save()
# 이메일 발송
signer = TimestampSigner() # 특정 정보를 암호화해서 사용가능 # 복호화가능, 장고 내부기능
signed_user_email = signer.sign(user.email)
signer_dump = signing.dumps(signed_user_email) # 서명된 이메일을 직렬화(Dumps)
# 암호해제하는 부분은 다른곳에서 사용
# print(signer_dump)
#
# decoded_user_email = signing.loads(signer_dump)
# print(decoded_user_email)
#
# email = signer.unsign(decoded_user_email, max_age= 60 * 30) # 시간제한
# print(email)
return render(self.request, # FBV 에서는 자체함수에서 request 를 가져오지만 상속받아온 self.request 쓰면됨
template_name='auth/signup_done.html',
context= {'user': user}
)
def verify_email(request):
code = request.GET.get('code', '') # 위에서 쿼리스트링으로 넘겨준 코드 받을수 있음 ?code=, 없으면 공백 None대신
signer = TimestampSigner()
try:
decoded_user_email=signing.loads(code)
email = signer.unsign(decoded_user_email, max_age=60 * 30)
except (TypeError, SignatureExpired):
return render(request, 'auth/not_verified.html')
user = get_object_or_404(User, email=email, is_active=False)
user.is_active=True
user.save()
# return redirect(reverse('login')) # 현재 만든것 없기에
return render(request, 'auth/email_verified_done.html',{'user':user})
from django.conf import settings
from django.core.mail import send_mail
def send_email(subject, message, to_email):
to_email= to_email if isinstance(to_email, list) else [to_email, ] # 단일 이메일도 리스트로 변환
send_mail(subject, message, settings.EMAIL_HOST_USER, to_email)
class SignupView(FormView):
template_name = 'auth/signup.html'
form_class = SignupForm
# success_url = reverse_lazy('signup_done') # success_url 은 reverse lazy
def form_valid(self, form):
user = form.save()
# 이메일 발송
signer = TimestampSigner() # 특정 정보를 암호화해서 사용가능 # 복호화가능
signed_user_email = signer.sign(user.email) # 암호화를 user.email 로한것
signer_dump = signing.dumps(signed_user_email) # 서명된 이메일을 직렬화하여 url에 포함
# 암호해제하는 부분은 다른곳에서 사용
# print(signer_dump)
#
# decoded_user_email = signing.loads(signer_dump)
# print(decoded_user_email)
#
# email = signer.unsign(decoded_user_email, max_age= 60 * 30) # 시간제한
# print(email)
# 이메일 인증을 위한 링크 생성
# 이제 이메일을 보내줘야하는데 어떤 링크로 들어와라
# 현재요청되어있는 경로로 코드 불러오기
# verify 에서 verify_email 로 가게된다
url = f'{self.request.scheme}://{self.request.META["HTTP_HOST"]}/verify/?code={signer_dump}'
if settings.DEBUG: # 디버그 모드일경우 콘솔에 출력 아닐경우 이메일 발송
print(url)
else:
subject = '[Pystagram]이메일 인증을 완료해주세요'
message = f'다음 링크를 클릭해주세요 <br><a href="{url}">{url}</a>'
send_email(subject, message, user.email)
return render(self.request, # FBV 에서는 자체함수에서 request 를 가져오지만 상속받아온 self.request 쓰면됨
template_name='auth/signup_done.html',
context= {'user': user}
)
from django.contrib.auth import get_user_model
User = get_user_model()
def verify_email(request):
# url 에서 code 파라미터 가져오기
code = request.GET.get('code', '') # 위에서 쿼리스트링으로 넘겨준 코드 받을수 있음 ?code=, 없으면 공백 None대신
signer = TimestampSigner() # 복호화시도
try:# 복호화시 오류가 날수도 있기에
decoded_user_email=signing.loads(code)
email = signer.unsign(decoded_user_email, max_age=60 * 30) # 직렬화된 데이터 복호화
except (TypeError, SignatureExpired): # 데이터가 올바르게 디코딩 안됬을경우, 30분 제한시간이 초과된 경우
return render(request, 'auth/not_verified.html')
user = get_object_or_404(User, email=email, is_active=False)
user.is_active=True
user.save()
# TODO: 나중에 Redirect 시키기
# return redirect(reverse('login')) # 현재 만든것 없기에
return render(request, 'auth/email_verified_done.html',{'user':user})
SignupView
)SignupForm
을 통해 사용자 계정을 생성 (is_active=False
)TimestampSigner
로 암호화 + 서명 후, URL 생성send_email()
)signup_done.html
)verify_email(request)
실행 (/verify/?code=암호화된_이메일
)signing.loads(code)
+ signer.unsign(decoded_user_email)
→ 이메일 복호화is_active=False
상태의 사용자 검색is_active=True
로 변경 후 저장email_verified_done.html
)SignupView
: 이메일을 암호화하여 인증 링크 전송verify_email
: 링크를 클릭하면 이메일 복호화 후 계정 활성화send_email
: Django send_mail()
을 사용하여 이메일 발송settings.py
: 네이버 SMTP를 통해 이메일 전송 설정class LoginForm(forms.Form):
email = forms.CharField(
label='이메일',
required = True,
widget=forms.EmailInput(
attrs={
'placeholder': 'example@example.com',
'class': 'form-control',
},
),
),
password = forms.CharField(
label='패스워드',
required = True,
widget=forms.PasswordInput(
attrs={
'placeholder':'password',
'class':'form-control',
}
)
)
# 폼데이터 검증을 수행하는 곳, 이메일과 비밀번호가 올바른지 확인하고, 사용자 인증을 처리하는 곳
def clean(self):
cleaned_data = super().clean()
email=cleaned_data.get('email')
password= cleaned_data.get('password')
# 사용자 인증 시도
self.user = authenticate(email=email, password=password)
if not self.user.is_active: # 유저가 비활성화된 경우
raise forms.ValidationError('유저가 인증되지 않습니다')
return cleaned_data
# forms.py 에 LoginForm 에 아래 내용을 추가한다
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user=None
# views.py 에
class LoginView(FormView):
template_name = 'auth/login.html'
form_class = LoginForm
# TODO: 나중에 메인페이지로 redirect 시키기
success_url = reverse_lazy('login')
# 방법 1
def form_valid(self, form): # LoginForm 을 불러올떄 이미 한번 불러옴 db에서
user=form.user # __init__이 설정되 있어서 가능
login(self.request, user)
next_page= self.request.GET.get('next')
if next_page:
return HttpResponseRedirect(next_page)
return HttpResponseRedirect(self.get_success_url())
<!-- 겹치는 부분 include/form.html 로 빼내기 -->
<!-- include/form.html -->
{% for field in form %}
<div class="form-group row mb-2">
<label class="form-label col-md-2" for="{{ field.auto_id }}">{{ field.label }}</label>
<div class="col-md-10">
{{ field }}
</div>
<!-- 장고에서 필드에서 오류가 났을떄 오류 띄워줌 -->
{% if field.errors %}
{% for error in field.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% endif %}
</div>
{% endfor %}
<!-- non_field_error 처리 -->
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="text-danger">{{ error }}</p>
{% endfor %}
{% endif %}
<!-- auth/signup.html -->
{% extends 'base.html' %}
{% block content %}
<div>
<h1 class="title">회원가입</h1>
<form method="POST">
{% csrf_token %}
{% include 'include/form.html' %}
<button class="btn btn-primary">회원가입</button>
</form>
</div>
{% endblock %}
<!-- auth/login.html -->
# url 연결
path('login/', member_views.LoginView.as_view(), name= 'login')
# setting.py Login_Url 설정
# urls
path('logout/', LogoutView.as_view(), name='logout'),
# settings.py
LOGOUT_REDIRECT_URL = '/'
{# base.html 수정 #}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Pystgram</title>
<link rel="stylesheet" href="{% static 'css/bootstrap.css' %}">
</head>
<body>
<nav class="d-flex justify-content-between py-2 px-4 bg-black text-white">
<div>
{# TODO: main 연결 #}
<a href="" class="text-decoration-none text-white">Pystagram</a>
</div>
<div class="text-end">
{% if request.user.is_authenticated %}
{{ request.user.nickname }}
<form action="{% url 'logout' %}" method="post" class="d-inline">
{% csrf_token %}
<button class="btn btn-dark btn-sm">로그아웃</button>
</form>
{% else %}
<a class="btn btn-dark btn-sm" href="{% url 'signup' %}">회원가입</a>
<a class="btn btn-dark btn-sm" href="{% url 'login' %}">로그인</a>
{% endif %}
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{% static 'js/bootstrap.bundle.js' %}"></script>
</body>
</html>
AuthenticationForm
vs. 커스텀 LoginForm (clean())
비교비교 항목 | Django AuthenticationForm 사용 | 커스텀 LoginForm (clean() ) 사용 |
---|---|---|
위치 | login() 함수에서 검증 수행 | clean() 메서드에서 검증 수행 |
검증 시점 | form.is_valid() 호출 시 자동 검증 | clean() 에서 직접 authenticate() 호출 |
사용 방식 | Django 기본 제공 (AuthenticationForm ) | 직접 폼을 정의 (LoginForm ) |
장점 | 간단하고 Django 내부 최적화 적용 | 로그인 로직을 원하는 대로 커스텀 가능 |
단점 | 폼을 커스텀하기 어려움 | 직접 clean() 을 구현해야 함 |
✅ 현재 login()
함수에서 AuthenticationForm
을 사용하여 로그인 검증을 form.is_valid()
로 처리하고 있음.
✅ LoginForm
에서 clean()
을 사용하면 같은 검증을 clean()
내부에서 처리할 수 있음.
✅ 둘 다 같은 기능을 수행하지만, clean()
을 사용하면 검증 로직을 Form
클래스 내부에서 캡슐화할 수 있음.
✔ 즉, AuthenticationForm
을 사용하는 현재 코드도 로그인 검증을 form
내부에서 처리하는 방식이며,
✔ LoginForm
에서 clean()
을 활용한 방식과 큰 차이는 없음! 🚀