사용 목적
- 회원 가입시 사용자의 이메일로 인증 코드를 발송하여
비 활성화된 계정을 인증- 사용자가 비밀 번호를 기억하지 못하는 경우, 인증 코드를 발송하여
새로운 비밀번호로 변경 기능 제공- 사용자가 이메일 정보를 수정하고 싶을 경우,
새로운 이메일로 인증 코드를 발송하여 이메일 변경 서비스를 제공
나의 이메일 인증 시스템 구현도
필드 설명
- verification_code
랜덤한 인증 코드를 저장할 필드,
해당 필드에 랜덤한 인증 코드를 저장하고,
사용자가 제출한 인증 코드와 비교하기 위하여 사용 된다.
- new_email
변경할 이메일 정보를 임시로 저장할 필드
- authentication_type
발급된 인증 코드 유형이 기존의 이메일을 인증하기 위한 인증 유형인지
또는 변경하기 위한 유형으로 발급된 인증 코드인지 구분하기 위하여
추가된 필드
- updated_at
공통 상속 목적으로 구현된 모델에서부터 상속된 필드
이메일 인증 코드를 발급 받은 시간으로부터
현재 인증하려는 시간 차이를 구하기 위해 추가 되었다.
모델 설계
class EmailVerification(CommonModel): user = models.OneToOneField("users.User", related_name="email_verification", on_delete=models.CASCADE, primary_key=True) verification_code = models.CharField('인증 코드', max_length=30, blank=True, null=True) new_email = models.EmailField("변경 이메일 주소", max_length=100, unique=True, blank=True, null=True) AUTHENTICATION_TYPE = [ # normal : 회원 인증, 비밀 번호 재 설정 ("normal", "일반"), # change : 이메일 변경 신청 ("change", "변경"), ] authentication_type = models.CharField("인증 유형", max_length=20, choices=AUTHENTICATION_TYPE, default="normal")
2단계 인증 후 앱 비밀번호 발급 받기
- 구글 계정 관리 접속
- 보안 접속
2단계 인증
2단계 인증후 앱 비밀번호 발급
사용 용도를 결정후 앱 비밀번호 생성
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = "smtp.gmail.com"
# 메일 host server
EMAIL_PORT = 587
# gmail과 통신 port
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER")
# 발신 이메일
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD")
# 앱 비밀번호
EMAIL_USE_TLS = True
# TLS 보완 방법
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
# 사이트와 관련된 자동 응답을 받을 이메일 주소
사용 목적
- 이메일 발송과 관련된 기능을 하나의 클래스에서 구성
- 추상 클래스를 이용하여 외부에서 접근하여 메서드로 이용
- views.py에 작성하지 않아 코드의 유지 보수성과 가독성 향상
- 어느 코드에서든 손쉽게 접근이 용이
이메일 템플릿
이메일 발송 모듈과 같은 경로에 위치하는 users app예
templates 디렉토리에 html 파일을 위치 시켰다.
- 템플릿 문법을 사용하여 전달할 메시지를 담았다.
코드 구현
from django.core.mail import EmailMessage from django.template.loader import render_to_string class EmailService: @classmethod def message_forwarding(cls, email, subject_message, content_message): title = "Choco The Coo" context = { 'subject_message': subject_message, 'content_message': content_message, } content = render_to_string('email_template.html', context) mail = EmailMessage(title, content, to=[email]) mail.content_subtype = "html" mail.send()
이해하기
- EmailMessage
장고에서 지원하는 이메일 발송을 위한 내장 기능- render_to_string
html 렌더링을 위한 장고 내장 기능- classmethod
외부에서 접근이 용이하게 추상 클래스를 사용- context
메서드의 인자값으로 제목과 내용을 전달 받고
딕셔너리의 key - value 형태로 담아내었다.- content
html 파일을 렌더링하며 값으로는 context로 작성한 딕셔너리를 전달.- EmailMessage
제목과 렌더링한 content, 발송받을 email 설정
한계성
- 이메일 발송 과정에서 실제로 존재하는 이메일인지
또는 존재하지 않는 이메일인지 확인할 수 없다.
모듈 관리의 장점
코드를 살펴보면 인자값으로
email, subject_message, content_message
세 가지를 받고 있는 것을 확인할 수 있다.
따라서 어느 코드에서건 외부에서 접근해서
발송받을 이메일과, 제목과 내용에 대한 string을 전달해 준다면
어떤 메시지든 담아서 발송할 수 있게 구현 되어 있다.
인증 코드 반환 하기
import string, random class EmailService: @classmethod def get_authentication_code(cls): random_value = string.ascii_letters + string.digits random_value = list(random_value) random.shuffle(random_value) code = "".join(random_value[:10]) return code
이해하기
* string.ascii_letters 알파벳 대문자, 소문자로 구성된 모든 요소를 저장(A....a....) * string.digits 숫자로 구성된 모든 요소를 저장 (0~9) * random.shuffle 리스트로 형변환한 값을 랜덤하게 뒤섞는다. * "".join(random_value[:10]) 슬라이스를 이용하여 10자 까지 잘라내고 join을 이용하여 문자열로 결합
나의 프로젝트에서 생각해 볼점
- 소셜 로그인 유저는 이메일 인증이 불 필요 하다.
- 회원 가입 기능에서 이미 이메일 유효성 검사를 마쳤다.
- 하지만 비밀 번호 찾기 기능 구현을 위해서는
한 번더 이메일 유효성 검사를 해야 할 필요가 있다.
- User Model과 가장 먼저 소개한
EmailVerification Model은 One To One 관계를 맺고 있다.
- 따라서 인증 코드를 발송하기 위해서 사용자는 이미
One To One Model이 생성이 안되 있을 수도
또는 이미 생성이 되 있을 수 있다.
@classmethod
def send_email_verification_code(cls, user, email, mod):
if user.login_type != 'normal':
# 소셜 계정으로 가입된 사용자일 경우 예외 처리
return [False, '소셜 계정으로 가입된 이메일 입니다.']
elif ValidatedData.validated_email(email) is not True:
return [False, '이메일 형식이 올바르지 않습니다.']
verification_code = cls.get_authentication_code()
try:
# 원투원 필드가 존재하면 인증 코드만 수집
email_verification = user.email_verification
email_verification.verification_code = verification_code
except users.models.EmailVerification.DoesNotExist:
# 원투원 필드가 존재하지 않으면 원투원 필드 생성
email_verification = users.models.EmailVerification(user=user, verification_code=verification_code)
email_verification.authentication_type = mod
email_verification.save()
subject_message = 'Choco The Coo has sent a verification email'
content_message = verification_code
EmailService.message_forwarding(email, subject_message, content_message)
return True
user.login_type != 'normal'
>> 발송하고자 하는 user 정보가 일반 회원이 아닌
소셜 계정 사용자 일경우 예외 처리를 하였다.
비밀 번호 찾기 기능을 위하여 사용자가 접근 할 경우
소셜 계정 사용자는 비밀번호가 필요 없기 때문이다.
ValidatedData.validated_email(email) is not True
>> 필자가 만든 이메일 유효성 검사 모듈이다.
정규식을 이용하여 이메일 형식이 올바르지 않는 경우
True 또는 False를 반환하게 구성했다.
cls.get_authentication_code()
>> 앞서 설명한 인증 코드를 반환 받는 과정 이다.
try:
# 원투원 필드가 존재하면 인증 코드만 수집
email_verification = user.email_verification
email_verification.verification_code = verification_code
except users.models.EmailVerification.DoesNotExist:
# 원투원 필드가 존재하지 않으면 원투원 필드 생성
email_verification = users.models.EmailVerification(
user=user, verification_code=verification_code
)
-------------------------------------------
email_verification = user.email_verification
>> 역참조를 이용하여 오브젝트 불러오기
만약 오브젝트가 원투원 필드가 존재하지 않는다면
DoesNotExist 오류가 발생한다.
-------------------------------------------
email_verification.verification_code = verification_code
>> 사용자의 인증 코드 필드에 새로운 인증 코드를 반환 한다.
-------------------------------------------
except users.models.EmailVerification.DoesNotExist:
email_verification = users.models.EmailVerification(
user=user, verification_code=verification_code
)
>> 원투원 필드가 존재하지 않는다면
유저 정보와 새로운 인증 코드를 저장 한다.
email_verification.authentication_type = mod
email_verification.save()
>> 인자값으로 mod를 받고 있는점이 생소할텐데
모델 설계에서 보인 인증 유형을 저장하기 위함 이다.
인증 코드 유형이
회원 인증 또는 비밀번호 찾기 기능을 위한 것이라면"normal"을 저장하고
이메일 변경을 위한 유형이라면 "change"를 저장하게 로직을 구현 했다.
subject_message = '인증 코드 발송하기 제목'
content_message = verification_code
EmailService.message_forwarding(email, subject_message, content_message)
>> 앞서 설명한 이메일 발송 모듈을 사용한 코드다.
제목과 내용, 사용자의 이메일 정보를 인자값으로 넘겨 주었다.
검증 요소
- 소셜 로그인 사용자가 이메일 인증을 요청 했을 경우
- 인증 코드를 발급 받지 않은 사용자가 인증을 요청 했을 경우
- 인증 유형이 다른 인증 코드로 인증을 요청 했을 경우
(회원 인증용 인증 코드로 이메일 변경 인증을 시도할 경우)- 인증 코드의 만료 기간이 초과 되었을 경우
- 제출한 인증 코드가 일치하지 않을 경우
from django.utils import timezone
from datetime import timedelta
.... 생략
@classmethod
def validated_email_verification_code(cls, user, request_verification_code, mod):
"""
이메일 인증 코드 유효성 검사
"""
if user.login_type != "normal":
# 소셜 로그인일 경우 이메일 인증이 필요 없음을 알림
return [False, '소셜 로그인 계정 입니다.']
try:
# 사용자에게 등록된 이메일 인증번호 불러오기
verification_code = user.email_verification.verification_code
except users.models.EmailVerification.DoesNotExist:
# 원투원 필드가 없을 경우 예외처리
return [False, '인증 코드를 발급 받아 주세요.']
if verification_code is None:
# 인증 코드를 발급받지 않았을 경우 예외 처리
return [False, '인증 코드를 발급 받아 주세요.']
elif user.email_verification.authentication_type != mod:
# 발급 받은 유형의 인증 코드를 다른 용도로 사용할 경우
return [False, '현재 발급 받은 인증 코드 유형이 올바르지 않습니다.']
elif not (timezone.now() - user.email_verification.updated_at) <= timedelta(minutes=5):
# 인증 유효 기간이 지났을 경우 예외 처리
user.email_verification.verification_code = None
user.email_verification.save()
return [False, '인증 코드 유효 기간이 만료되었습니다.']
elif not verification_code == request_verification_code:
# 사용자가 입력한 이메일 인증번호와, 등록된 이메일 인증번호가 일치하지 않을 경우 예외처리
return [False, '인증 코드가 일치하지 않습니다.']
else:
return True
def ....(cls, user, request_verification_code, mod):
>> 인자값
user : 사용자 정보
request_verification_code: 사용자가 작성한 인증 코드
mod : 사용자가 접근한 인증 유형(계정인증,이메일변경)
if user.login_type != "normal":
>> 소셜 로그인 사용자는 이메일 인증 절차가 필요 없다.
소셜 계정으로 로그인 하면 되기 때문
휴면 계정으로 전환된 소셜 계정은, 로그인시 is_active를 활성화 되게 구성 했다.
try:
# 사용자에게 등록된 이메일 인증번호 불러오기
verification_code = user.email_verification.verification_code
except users.models.EmailVerification.DoesNotExist:
# 원투원 필드가 없을 경우 예외처리
return [False, '인증 코드를 발급 받아 주세요.']
if verification_code is None:
# 인증 코드를 발급받지 않았을 경우 예외 처리
return [False, '인증 코드를 발급 받아 주세요.']
>> 사용자의 원투원 모델에 인증 코드가 존재 하는지 판단 코드
원투원 필드가 존재하지 않는다면 DoesNotExist 오류가 발생 하므로
예외처리를 해 주었다.
elif user.email_verification.authentication_type != mod:
# 발급 받은 유형의 인증 코드를 다른 용도로 사용할 경우
return [False, '현재 발급 받은 인증 코드 유형이 올바르지 않습니다.']
>> APIView로 인증 유형이 비밀번호 찾기, 휴면 계정 전환, 이메일 변경
의 타입이 존재하게 된다.
인증 코드를 받았을때 저장한 인증 유형과
사용자가 접근한 API의 인증 유형이 다르다면 예외 처리를 해 주었다.
elif not (timezone.now() - user.email_verification.updated_at) <= timedelta(minutes=5):
# 인증 유효 기간이 지났을 경우 예외 처리
user.email_verification.verification_code = None
user.email_verification.save()
return [False, '인증 코드 유효 기간이 만료되었습니다.']
>> updated_at으로 마지막 수정일과
timezone.now의 차이값이
timedelta(minutes=5), 5분 이상이라면 예외처리를 해 주었다.