Django (CustomUser, 환경변수, Email, Login)

Haks.·2025년 1월 29일
0

How to use

목록 보기
17/32

📝 인스타 pj

📌 DB 구조

  • 유저 (email 기반)
    • 소셜 로그인
    • 팔로우, 팔로워
  • 포스트(블로그랑 비슷)
    • 댓글
    • 좋아요(Like)
    • 태그(#장고)

기능

회원관련

  • 회원가입, 로그인, 비밀번호찾기...
  • 소셜로그인
  • 팔로우, 팔로우 취소
  • 팔로우/팔로워 수 및 목록 조회

포스트

  • 글 생성, 수정, 삭제, 나만보기(친한사람만 보기)/태그기능
  • 댓글 작성, 수정, 삭제
  • 라이크
  • 무한스크롤 기능(포스트,댓글)

ETC

  • DM, 릴스, (여기선 안만들 거임)

📝 Django 기본설정 변경

📝 Custom User Model

  • 기본 User 모델은 username 기반인데 email 기반으로 변경할 것임
  • migrations 하기전에 커스터마이징을 해줘야함 했으면 db.sqlite3 지우고 동작
  1. python manage.py startapp member
  2. models.py 에서 커스터마이징을 실행 User 테이블 불러오기
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)
  1. 기존의 username 기반을 eamil 기반으로 수정하기 위해서 manager를 오버라이드 할것임
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 로 사용 가능
  1. 그때그때 필요한것 쓰면됨 외울필요는 없음 닉네임이 주가되게 수정하거나 등등
  2. settings.py 제일 밑에 아래 코드 추가
AUTH_USER_MODEL='member.User' # 이번 장고프로젝트에서 User는 내가 만든 User로 쓴다는 선언
# INSTALLED_APP 에 member 추가
  1. poetry add django-extensions, ipython -> django_extensions 인설트앱 추가
  2. python manage.py createsuperuser -> email 로 만들어 지는 것 확인

📝 bootstrap 사용

  1. config/settings.py, bootstrap 사용을 위해 STATIC_URL 수정, static 파일 생성(최상위)
STATIC_DIR = BASE_DIR / 'static'
STATICFILES_DIRS = [
	STATIC_DIR,
]
STATIC_ROOT = BASE_DIR / '.static_root'
  1. bootstarp -> 문서 -> 다운로드 -> 컴파일된 css,js 다운 -> 압축해제후 static 폴더로 이동

📝 회원가입 기능 제작

  1. member/forms.py 생성
  2. 회원가입 만들때 UserCreationForm 을써서 사용 했으나 email을 기준으로 하는 기본 모델을 바꿧기에 수정이 필요
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',
                }
            )
        }
  1. setting.py template 추가 BASE_DIR/'template'
  2. base.html 생성 {% load static %}
{% 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>
  1. member/views.py singup 작성, FormView작성, auth/signup.html 생성, config/urls.py 수정
# 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 %}
  1. html 코드에서 오류처리 해놨기 때문에 해당 관련된 패스워드 오류 출력
  2. 생성을 하면 success_url이 없어서 에러가 남, 그리고 모델에서 email 이 들어오면 is_active 가 활성화가 안되는 것이 default 이다 그래서 회원가입이 완료된 후 이메일을 확인하는 페이지를 만들자. auth/signup_done.html
{% extends 'base.html' %}
{% block content %}
    <div class="col-8 offset-2">
        <h1>회원가입이 완료되었습니다.</h1>
        <p>
            이메일 인증을 하여 회원가입을 완료해주세요.
        </p>
    </div>
{% endblock %}
  1. urls.py 수정, views.py 수정
# 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
    
  1. DB.browser 확이니 생기지 않은것 확인 -> form 에서 제대로 처리되지 않고 있음
    def form_valid(self, form):
        form.save()
        return HttpResponseRedirect(self.get_success_url())
  1. 닉네임이 없어서 생긴것 처리완료
  2. 현재 signup/done 페이지가 게속 들어가짐 랜더링 해줘야함
  3. success.url 로 가는 것을 없애주고 render를 사용해서 signup_done.html 랜더링되게 수정
# 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 로 관리하지만 다른 방식으로 한번 해보기
  1. 최상위 디렉토리 생성 .config_secret/secret.json 생성 -> 숨겨진 파일, 깃을 쓸경우 .gitignore에 추가해주기 .config_secret/*
  2. secret.json 에 유저 닉네임 관련 입력, 이런식으로 작성 회사엔 보통 환경변수가 갖춰저있으니 맞춰서 사용
{
  "DB": {
    "HOST" : "ABCD",
    "USER" : "SAD",
    "PASSWORD": "Asss"
  },
    "DB2": {
    "HOST" : "ABCD2",
    "USER" : "SAD2",
    "PASSWORD": "Asss2"
  }
}
  1. settings.py 수정
# 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시 호스트가 잘나오나 확인
  1. SECRET_KEY ='' 되어있는것이 중요한데 이것을 환경변수로 빼줄 수 있다. 이것을 secret.json 에 넣자
{
  "DJANGO_SECRET_KEY" : "django-insecure--_o1uke5zi&9+on4o(fr-$5k83uam2f14@gdeu(6)e32qqvhv$"
}
  1. setting.py에 SECRET_KEY 수정
SECRET_KEY = SECRET['DJANGO_SECRET_KEY']
  1. 환경변수 설정 완료 (시크릿 키 환경변수에 넣기)

📝 네이버로 인증메일 전송

  • 네이버로 확인 이메일을 장고를 통해 보낼때 설정
  1. 네이버 이메일 -> 왼쪽 카테고리 환경설정 -> POP3/IMAP 설정 -> 사용함 -> IMAP 사용함 -> 저장 // 우리는 SMTP 만 쓸거임, 맨밑에 있는 설정을 토대로 하는거임
  2. secret.json email 저장
{
  "DJANGO_SECRET_KEY": "django-insecure--_o1uke5zi&9+on4o(fr-$5k83uam2f14@gdeu(6)e32qqvhv$",
  "email": {
    "user": "본인 이메일",
    "password" : "PXB1Q889R1LY" # 앱비밀번호
  }
}
  1. setting.py 제일밑에 네이버 smtp 에 맞춰서 작성
# 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"]
  1. 2단계 인증으로인해 앱비밀번호로 바꿔서 작성
  2. python manage.py shell_plus 를 통해 메일 보내보기
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)
  1. member/views.py form_valid 한 상태에서 이메일 발송이 이뤄줘야함
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}
                      )
  1. 인증하는 함수 생성
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})
  1. 이메일 인증하고 출력되는 url 로 들어가면 이메일 인증 완료 라고나옴
  2. db.browser 로 is_activate 가 활성화된걸 확인
  3. 이메일 보내는 기능 만들기, 회원가입 말고도 존재할 수도 있으니 최상위 폴더에 이메일 보내는 기능을 만들자. utils/email.py
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)
  1. 최종 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()

        # 이메일 발송
        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})
  • 이메일이 형식을 올바르게 하지 않으면 네이버에서 받지 않음
    • hak@hak.com => hak이라는 도메인을 가진 곳이 없음 안받음
    • hak2@naver.com => hak2 라는 주소를 가진 사라밍 없으면 발송은 하나 올바르게 보내지 않음

📌 전체 코드 흐름

1️⃣ 회원가입 (SignupView)

  1. SignupForm을 통해 사용자 계정을 생성 (is_active=False)
  2. 이메일 주소를 TimestampSigner암호화 + 서명 후, URL 생성
  3. 이메일 인증 링크 전송 (send_email())
  4. 회원가입 완료 페이지 렌더링 (signup_done.html)

2️⃣ 사용자가 이메일 인증 링크 클릭

  1. verify_email(request) 실행 (/verify/?code=암호화된_이메일)
  2. url에 등록되어 있는 /verify를 통해 def verify_email 함수로 들어감
  3. signing.loads(code) + signer.unsign(decoded_user_email) → 이메일 복호화
  4. is_active=False 상태의 사용자 검색
  5. is_active=True로 변경 후 저장
  6. 인증 완료 페이지 렌더링 (email_verified_done.html)

🚀 결론

  • SignupView: 이메일을 암호화하여 인증 링크 전송
  • verify_email: 링크를 클릭하면 이메일 복호화 후 계정 활성화
  • send_email: Django send_mail()을 사용하여 이메일 발송
  • settings.py: 네이버 SMTP를 통해 이메일 전송 설정

📝 로그인, 로그아웃 기능

  1. member/forms.py 생성
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
  1. views.py 제작
# 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())
  1. login.html 생성
<!-- 겹치는 부분 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 설정
  1. logout 페이지
# urls
    path('logout/', LogoutView.as_view(), name='logout'),
# settings.py
LOGOUT_REDIRECT_URL = '/'
  1. logout은 post로 보냈어야 했다.
{# 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>
  1. 동작과정
    1. 사용자가 로그인 폼 제출
    2. 장고가 clean()을 실행하여 데이터 검증
      • 유효한 경우 : cleaned_data 가 반환되고 form_valid() 실행
      • 잘못된 경우 : ValidationError 발생 -> 로그인 페이지에 오류메시지 표시

📌 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()을 활용한 방식과 큰 차이는 없음! 🚀

0개의 댓글

관련 채용 정보