메일기능을 활용한 본인인증 및 비밀번호 재설정
이제 SMTP 가 무엇인지는 알았으니 이걸 이용해 여러 기능들을 구현할 차례이다
우선 메일기능을 활용해 여러 사이트에 있는 이메일 본인인증 기능을 구현 할 생각이다
처음써보는 기능이라 당황했을테지만 어느사이트를 이용하든 이메일관련한 본인인증은 자주 사용되며 그만큼 레퍼런스가 많아 편하게 구현할 수 있을 것이라 생각할 것이다.
또한 Django 는 설계철학에 맞게 이메일을통한 본인인증 혹은 비밀번호 재설정이 자주쓰이는 기능이란걸 알고있어
미리 로직을 구성해둬서 사용할 수 있게 해준다.
Django의 기본 이메일 인증 및 비밀번호 재설정 기능
1. 비밀번호 재설정 기능 (Password Reset)
Django의 django.contrib.auth 앱에서는 아래와 같은 비밀번호 관련 View와 URL 패턴이 기본 제공됩니다:
기능 | 기본 뷰 | 설명 |
비밀번호 재설정 폼 | PasswordResetView | 이메일 입력 받아서 재설정 메일 전송 |
이메일 전송 완료 페이지 | PasswordResetDoneView | 이메일 전송 후 보여주는 페이지 |
비밀번호 재설정 링크 접속 처리 | PasswordResetConfirmView | 사용자가 받은 메일의 링크로 접속했을 때 처리 |
비밀번호 변경 완료 페이지 | PasswordResetCompleteView | 새 비밀번호 설정 완료 후 보여주는 페이지 |
URL 설정 예시
from django.contrib.auth import views as auth_views
from django.urls import path
urlpatterns = [
path('password_reset/', auth_views.PasswordResetView.as_view(), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(), name='password_reset_complete'),
]
템플릿만 커스터마이징하면 바로 사용 가능
- registration/password_reset_form.html
- registration/password_reset_done.html
- registration/password_reset_confirm.html
- registration/password_reset_complete.html
이런 템플릿 이름으로 만들기만 하면 Django가 자동으로 찾아서 사용합니다.
2. 이메일을 통한 회원가입 인증 (이메일 검증)
이건 기본 Django는 제공하지 않지만, django-allauth 같은 외부 패키지를 쓰면 아주 간단하게 구현할 수 있다.
예: django-allauth 사용 시
- 회원가입하면 인증 메일 자동 전송
- 이메일 인증을 마쳐야 로그인 가능하도록 설정 가능
- 소셜 로그인(Kakao, Google 등)도 연동 쉬움
설정 예시:
ACCOUNT_EMAIL_VERIFICATION = "mandatory" # 이메일 인증 필수
ACCOUNT_EMAIL_REQUIRED = True
뭐야 너무 쉬운데?
라고 생각 할수도있다.
하지만 우리는 강의때 Django 의 장,단점을 배운걸 떠올려 볼 수 있다.
설계철학에 맞게 미리 만들어둔게 많아서 개발자의 편의성이 좋은점과 동시에
자율성이 떨어진다는 점이다
이런 Django 안의 contrib 안의 auth 안의 기존 view 나 url패턴을 사용하고자 한다면
Django 의 기본 AbstractUser 를 상속받은 커스텀 유저모델을 그대로 사용해야 한다.
하지만 우리는 기존 User 모델을 우리프로젝트에 맞게 추후 확장성을 고려해
AbstractBaseUser 를 상속받아 모델을 처음부터 직접 구성하였으며 그에따른 기존기능들을 직접 구현해야만 한다.
그래서 이 방법을 소개해보고자 한다.
먼저 큰 흐름을 설명하자면 내가 진행한 프로젝트의 커밋메세지로 이해할수 있을것이다.
먼저 흐름을 알기위해 현재 모델구성이 어떻게 되어있나 살펴보았다.
유저기능을 담당해준 야무진 팀원이
email_verified 라는 필드를 기본값 False 로 만들어 두었는데
이걸 사용해서 본인인증기능을 구현해라!
라는 말을 하는것 같았다.
그래서 만들어진걸 바탕으로 로직을 생각해보며 시퀀스 다이어그램을 그려보았다
기능의 플로우는 이런 흐름으로 진행될 예정이다.
코드짜기
SMTP 서버를 따로 만들기보단 Gmail 에서 하루 500건 무료로 사용할수 있게 해줘서 Gmail 의 SMTP 서버를 사용하기로 결정하였다.
메일용 계정을 하나만들고 APP비밀번호 까지 생성해 준 후 관련 설정을 해주었다.
Settings.py
# Gmail SMTP서버 관련 설정
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = env("EMAIL_PORT")
EMAIL_HOST_USER = env("EMAIL_ID")
EMAIL_HOST_PASSWORD = env("EMAIL_APP_PW")
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = f"AInfo <{EMAIL_HOST_USER}>"
EMAIL_TIMEOUT = 10
그 후 기존 Django 안에있는 view 로직을 참고해서 만들어 보려는데 uid 와 token 으로 사용자를 판별하는 로직으로 구성되있단걸 알았으니 따라가고자 했다.
원래 내가 경험하던 본인인증방식과 구상했던방법은
- 난수생성으로인한 랜덤코드 발송후 expire 시간을 주고 redis 에 저장
- 사용자가 알맞은 랜덤코드를 입력하면 본인인증 성공
이런 방식이지만 나도 uid 와 token 을 이용해 만들어보고자 메일 템플릿을 작성했다.
# accounts/templates/account/activate_email.html
안녕하세요 {{ user.email }}님
이메일 인증을 완료하려면 아래 링크를 클릭하세요
"http://{{ domain }}/api/v1/accounts/activate/{{ uid }}/{{ token }}/"
- 너무 간단해보이는 이유는 메일발송시간이 너무 오래걸린다는 피드백 때문
- Celery 도입 전 작업자체를 가볍게 하기 위함이 목적
- Celery 를 통한 백그라운드에서 비동기 처리로 인한 최적화로 발송시간을 87% 감소시켜 다음버전에서 이쁘게 꾸밀 예정
사용자를 안전하게 식별하기 위해
uid 와 token 을 사용해 인증 로직을 처리한다
- uid : 사용자의 ID 를 base64 로 인코딩한 값 -> urlsafe_base64_encode(force_bytes(user.pk))
- toekn : Django 의 PasswordResetTokenGenerator 를 사용해 생성한 유효한 토큰 값
이제 해당 링크를 누르면 저 URI 주소를 통한 요청이 들어오게되니 해당 요청을 처리하기위한 View 단에서의 로직과 매핑하는 URL패턴을 작성해야한다.
# accounts/urls.py
urlpatterns = [] + [
path("activate/<uid>/<token>/", ActivateEmailView.as_view(), name="activate_email"),
]
그후 본인인증을 위한 token 을 생성하기 위한 코드를 작성한다
# accounts/tokens.py
from django.contrib.auth.tokens import PasswordResetTokenGenerator
class CreateToken(PasswordResetTokenGenerator):
"""
Description: 유저의 pk, email_verified 와 현재시간으로 토큰을 생성
- PasswordResetTokenGenerator 의 _make_hash_value 함수를 오버라이딩
- 악의적인 사용자가 인증 링크를 조작하지 않도록 보호하기 위함이 목적
- 이메일 인증 링크에 포함시킬거임
"""
def _make_hash_value(self, user, timestamp):
return str(user.pk) + str(timestamp) + str(user.email_verified)
token_for_verify_mail = CreateToken()
그후 기존 회원가입 로직 마지막에 메일발송을 하는 로직을 추가한다.
# accounts/views.py
def perform_create(self, serializer):
"""
Description: 회원가입후 이메일 인증 메일 발송을 위한 함수
- perform_create 메서드를 오버라이딩
- uid와 tokens.py 에서 작성한 CreateToken 을 통해 만든 token 을 인증메일에 포함
"""
user = serializer.save()
# 이메일 인증을 위한 토큰 생성
token = token_for_verify_mail.make_token(user)
uid = urlsafe_base64_encode(force_bytes(user.pk))
# 인증 URL 생성
current_site = get_current_site(self.request) # 현재 사이트 도메인 가져오기
mail_subject = '이메일 인증을 완료해주세요.'
message = render_to_string('account/activate_email.html', {
'user': user,
'domain': current_site.domain,
'uid': uid,
'token': token,
})
send_mail(mail_subject, message, 'ainfo.ai.kr@gmail.com', [user.email])
return Response(
{"message": "회원가입 완료, 이메일 인증을 확인해주세요."},
status=status.HTTP_201_CREATED,
)
그후 메일로보낸 인증링크를 통해 들어온 요청을 처리하는 로직을 추가하면 된다.
class ActivateEmailView(APIView):
"""
Description: 메일로보낸 인증링크를 통해 들어온요청 을 처리하는 클래스
- uid, token 과같이 보낸 인증메일을 통해 판별한후 email_verified 를 True 로 변경
"""
permission_classes = [permissions.AllowAny]
def get(self, request, uid, token):
try:
# uid 디코딩
uid_decoded = urlsafe_base64_decode(uid).decode()
user = User.objects.get(pk=uid_decoded)
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
user = None
# 토큰 확인
if user and token_for_verify_mail.check_token(user, token):
user.email_verified = True
user.save()
return Response({"message": "이메일 인증이 완료되었습니다."}, status=status.HTTP_200_OK)
else:
return Response({"error": "잘못된 인증 링크입니다."}, status=status.HTTP_400_BAD_REQUEST)
위와 같은방식으로 진행하면 메일관련기능은 끝이나고 어느정도는 직접구현하면서 감을 익혔으니
메일과 관련된 추가기능이나 수정 등을 조금더 수월하게 할 수 있을것이다.
문제라면 비동기처리를 안하게 되어서 회원가입시마다 4700ms 정도 걸리는데
이는 Celery 를 통해 해결하였으며 관련내용은 추후에 포스팅 할 예정이다.
'Project > AInfo' 카테고리의 다른 글
[WebSocket] WebSocket 이란? (Django Channels) (0) | 2025.03.14 |
---|---|
[Celery] Celery 란? (0) | 2025.03.06 |
[SMTP] SMTP 란? (0) | 2025.03.02 |