본문 바로가기

개발일지/TIL

TIL 23-05-10 drf 팀 프로젝트 - 이메일 인증

1.drf 팀 프로젝트 - 이메일 인증

문제점

유저 회원가입을 만드는 중, 이메일 인증의 필요성이 생김

시도해 본 것들

dj-rest-auth 패키지 사용 시도

이메일을 받을 수 있지만, 기존에 작성한 회원 가입이 아닌 패키지에서 제공하는 회원 가입을 사용하면서 필수적인 값을 넘길 방법을 찾지 못함

settings.py에서 시크릿 키처럼 관리해야 할 항목 추가

EMAIL_HOST_USER = "이메일"
EMAIL_HOST_PASSWORD = "비밀번호"

인증 이메일을 전송하는 이메일과, 비밀번호

# views.py 

# 이메일 인증 view
class ConfirmEmailView(APIView):
    permission_classes = [AllowAny]

    def get(self, *args, **kwargs):
        self.object = confirmation = self.get_object()
        confirmation.confirm(self.request)
        # A React Router Route will handle the failure scenario
        return HttpResponseRedirect('/') # 인증성공

    def get_object(self, queryset=None):
        key = self.kwargs['key']
        email_confirmation = EmailConfirmationHMAC.from_key(key)
        if not email_confirmation:
            if queryset is None:
                queryset = self.get_queryset()
            try:
                email_confirmation = queryset.get(key=key.lower())
            except EmailConfirmation.DoesNotExist:
                # A React Router Route will handle the failure scenario
                return HttpResponseRedirect('/') # 인증실패
        return email_confirmation

    def get_queryset(self):
        qs = EmailConfirmation.objects.all_valid()
        qs = qs.select_related("email_address__user")
        return qs
# urls.py

# dj-reset-auth 패키지 활용하기 
    path('dj-rest-auth/', include('dj_rest_auth.urls')),
    path('dj-rest-auth/registration/', include('dj_rest_auth.registration.urls')),
    re_path(r'^account-confirm-email/$', VerifyEmailView.as_view(), name='account_email_verification_sent'),
    # 유저가 클릭한 이메일(=링크) 확인
    re_path(r'^account-confirm-email/(?P<key>[-:\\w]+)/$', views.ConfirmEmailView.as_view(), name='account_confirm_email'),

이해한 코드를 사용한 게 아니라서 커스텀의 어려움이 생김

포기 후 다른 방법 찾기

django의 EmailMessage 사용하기

# settings.py
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST = "smtp.gmail.com"
EMAIL_HOST_USER = "이메일"
EMAIL_HOST_PASSWORD = "비밀번호"
DEFAULT_FROM_MAIL = EMAIL_HOST_USER

이메일을 사용하기 위한 기본적인 설정

manage.py shell 에서 이메일 테스트

from django.core.mail import EmailMessage

email = EmailMessage('title', 'content', to=['이메일'])
email.send()

shell 에서 이 코드를 실행했을 때 1이 나오면 정상적으로 email이 전송 된 상태다.

from django.core.mail import EmailMessage

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "__all__"
        extra_kwargs = {
            "followings": {
                "read_only": True,
            },
        }

    def create(self, validated_data):
        password = validated_data.pop("password")
        user = User(**validated_data)
        user.set_password(password)
        user.save()

        **to_email = user.email
        email = EmailMessage("title", "content", to=[to_email])
        email.send()**
        return user

회원 가입을 진행하는 UserSerializer에서 email 전송 테스트

tokens.py 생성

from django.contrib.auth.tokens import PasswordResetTokenGenerator

class AccountActivationToken(PasswordResetTokenGenerator):
    def _make_hash_value(self, user, timestamp):
        return user.pk + timestamp + user.is_active

account_activation_token = AccountActivationToken()

이메일 인증에서 핵심이 되는 인증키를 만드는 함수

serializer 수정하기

from django.core.mail import EmailMessage
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode

from .tokens import account_activation_token

class UserSerializer(serializers.ModelSerializer):
# 추가 내용 
	# url에 포함될 user.id 에러 방지용 encoding하기
	uidb64 = urlsafe_base64_encode(force_bytes(user.id))
	        # tokens.py에서 함수 호출
	        token = account_activation_token.make_token(user)
	        to_email = user.email
	        email = EmailMessage(
	            "AOA 술술술 이메일 인증",
	            f"<http://127.0.0.1:8000/users/activate/{uidb64}/{token}>",
	            to=[to_email],
	        )
	        email.send()

urls 작성

urlpatterns = [
    path(
        "activate/<str:uidb64>/<str:token>/",
        views.ActivateView.as_view(),
        name="activate_view",
    ),
]

이메일 인증 view 작성

from django.utils.encoding import force_str
from django.utils.http import urlsafe_base64_decode

class ActivateView(APIView):
    def get(self, request, uidb64, token):
        print("activate")
        try:
            uid = force_str(urlsafe_base64_decode(uidb64))
            user = User.objects.get(pk=uid)
            if account_activation_token.check_token(user, token):
								# 최초 회원가입 시 is_active=False, 인증 시 True로 변경 
                User.objects.filter(pk=uid).update(is_active=True)
                return Response({"인증 완료!"})
            return Response({"error": "AUTH_FAIL"}, status=400)
        except KeyError:
            return Response({"error": "KEY_ERROR"}, status=400)

참고했던 자료가 옛날 자료여서 force_text 사용 ← django 4.0 이상부터 force_str로 사용

해결 방법

EmailMessage 사용하기

from django.core.mail import EmailMessage

알게 된 점

잘 모르는 내용을 시도할 때는 가장 기초적인 방법을 시도하는 게 난이도가 쉬운 건 당연하고 , 작성하고 문제를 해결하면서 배울 수 있는 점이 좋다는 것을 체감했다.

코드 자체가 어렵고 패키지에 너무 많은 내용이 담겨있어 어디서 문제가 발생하는지 알 수가 없었다.

하지만 EmailMessage는 참고하던 코드가 six나 force_text를 사용해도 중요한 코드가 어렵지 않아서 하나하나 찾아보고 직접 실행하며 수정할 수 있었고 코드의 흐름을 파악할 수 있었다.

 

1.drf 팀 프로젝트 - secrets.json

문제점

이메일 인증 기능을 구현한 후 github에 push하기 전 이메일과 비밀번호를 보호해야함

시도해 본 것들

secrets.json 활용하기

secrets.json

{
    "SECRET_KEY": "시크릿 키",
    "EMAIL": "이메일",
    "PASSWORD": "비밀번호"
}

settings.py

EMAIL_HOST_USER = get_secret("EMAIL")
EMAIL_HOST_PASSWORD = get_secret("PASSWORD")

SECRET_KEY를 보호하는 과정에서

secret_file = os.path.join(BASE_DIR, "secrets.json")

with open(secret_file) as f:
    secrets = json.loads(f.read())

def get_secret(setting, secrets=secrets):
    try:
        return secrets[setting]
    except KeyError:
        error_msg = "Set the {} environment variable".format(setting)
        raise ImproperlyConfigured(error_msg)

이 코드들이 존재해서 처음에 설정하고 난 뒤는 get_secret을 통해서 간단하게 호출할 수 있다.

알게 된 점

이번 프로젝트를 시작하면서 처음 SECRET_KEY를 관리해서 아직 막연한 걱정이 있었는데 보호할 데이터를 추가하는 것은 훨씬 쉽게 가능했다.