[Choeaein] 회원가입 시에 이메일 인증 링크 전송하기

silverKi·2023년 9월 29일
0

Choeaein(MyFavor)

목록 보기
2/4

V1.MyFavor 프로젝트를 진행시에 해보고 싶었지만 시간이 없어서 시도해보지 못한 이메일 인증링크를 v2.Choeaein 프로젝트에서 진행한 과정을 서술 하고자 한다.


✨ 왜 이메일 인증링크를 구현하고자 하였나?

최애인 프로젝트는 회원가입 후에 사용자는 본인의 이메일과 비밀번호를 입력하여 로그인을 시도 하게 된다.

만약 사용자가 회원가입시에 입력한 이메일이 유효한 이메일이 아닌 경우 여러 문제점들이 있겠지만, 가장 큰 문제는 사용자가 어려움에 처한경우 문제 해결이나 지원 요청에 대한 응답을 받을 수 없게 된다. 문제해결을 하지 못한 사용자는 결국 이탈 할 수 밖에 없을 것이다.

따라서 사용자가 회원가입시에 입력한 본인의 이메일을 인증하여 인증이 완료된 이메일 계정인 경우에만, 나머지 회원 가입 절차를 할 수 있게 하였다.


✨ 이메일 인증을 하기 위한 form을 만든 이유

이메일 인증시에 사용자가 입력한 값에 메일을 전송하여 인증링크를 전송하게 되면 사용자에게 backend의 주소가 노출이 되는 문제를 발견하여, 이를 개선하고자 하였다.

물론 인증링크의 유효 시간을 걸어 timeout하는 경우도 있겠지만, bacakend의 주소를 보여준다는게 많은 보안적인 측면에서 별로 좋지 않은 것 같다. (사용자가 어떤 작업을 할 지 모르기 때문)

backend주소가 user에게 노출되는 모습

✅ 이메일 인증을 하기 위한 form 만들기

** google SMTP 만드는 방법은 생략합니다.

<!DOCTYPE html>
<table style="font-family: 'Apple SD Gothic Neo', 'sans-serif' !important; width: 540px; border-top: 4px solid #02b875; margin: 100px auto; padding: 30px 0; box-sizing: border-box;">
    <tr>
        <td style="margin: 0; padding: 0 5px; font-size: 28px; font-weight: 400;">
            <span style="font-size: 15px; margin: 0 0 10px 3px;">최애인</span><br />
            <span style="color: #02b875;">메일인증</span> 안내입니다.
        </td>
    </tr>
    <tr>
        <td style="font-size: 16px; line-height: 26px; margin-top: 50px; padding: 15px 6px; border-top: 1px solid #DDD;">
            <p style="color: #555;">
            안녕하세요. 최애인 입니다.<br />
            아래 <b style="color: #02b875;">'메일 인증'</b> 버튼을 클릭하여 회원가입을 완료해 주세요.<br />
            감사합니다.
            </hr>
        </td>
    </tr>
    <tr>
        <td>
            <a href="{{ auth_url }}" target="_blank" style="display: inline-block; width: 210px; height: 45px; margin: 30px 5px 40px; background: #02b875; line-height: 45px; vertical-align: middle; font-size: 16px; text-decoration: none; text-align: center;">
                <button style="width: 100%; height: 100%; background: none; color: #FFF; border: none; cursor: pointer; padding: 0;">
                    메일 인증
                </button>
            </a>
        </td>
        
    </tr>
    <tr>
        <td style="border-top: 1px solid #DDD; padding: 5px;">
            <p style="font-size: 13px; line-height: 21px; color: #555;">
                만약 버튼이 정상적으로 클릭되지 않는다면, 아래 링크를 복사하여 접속해 주세요.<br />
                {$auth_url}
            </p>
        </td>
    </tr>
</table>
</html>

🧩 단계별 과정: back - front 소통하기

이제 auth_url을 클릭하면 frontend url로 redirect할 수 있도록 한다.
Frontend_url ="xxx"를 환경변수로 설정한 후에 auth_url에 서버의 인증 링크를 담고, 유저가 이를 클릭하면 설정한 frontend_url로 redirect해준다.
front에서는 URL과 토큰을 처리하는 로직을 추가한다.
이메일에서 받은 URL에서 토큰과 사용자 ID를 추출한 후, 이 정보를 백엔드에 POST 요청으로 전송하여 이메일을 검증할 수 있도록 하였다.

from django.contrib.auth.views import PasswordResetConfirmView
from django.urls import path
from . import views

urlpatterns=[
    path("signup/step1/", views.step1_SignUP.as_view(), name="email_signUp_step1"),
    path("verify/<pk>/<token>/", views.EmailVerification.as_view(), name="email_verification"),
    path("signup/step2/<pk>/<token>/", views.step2_SignUp.as_view(), name="email_signUp_step2"),
    
    path("login/", views.Login.as_view(), name="try_login"),  
    path("logout/", views.Logout.as_view()),  
    
    path("findID/", views.FindID.as_view()),
    path("findPW/", views.FindPW.as_view()),
    path("reset/<pk>/<token>/", views.PWResetConfirm.as_view(), name='password_reset'),
    path("changePW/", views.ChangePW.as_view()),  
]
class step1_SignUP(APIView):#회원가입
    def get(self, request):
        return Response({"email을 입력해주세요."}, status=status.HTTP_200_OK)

    def post(self, request):
        email=request.data.get("email")
        print("email", email)
        if not email:
            raise AuthenticationFailed({"error":"유효한 이메일 형식을 입력해 주세요."}, status=status.HTTP_403_FORBIDDEN)
        
        try:
            user=User.objects.get(email=email)
            print("해당 이메일 주소가 db에 존재함.")
            if not user.pick:
                user.delete()
                return Response({"message":"회원가입 절차를 완료하지 않은 동일 email을 갖는 user를 삭제함."}, 
                                status=status.HTTP_204_NO_CONTENT)
            else:
                return Response({"message":"이미 회원가입 절차를 완료한 사용자 입니다."}, status=status.HTTP_400_BAD_REQUEST)
        except:
            user=User.objects.create(
                email=email,
                name="myfavor",
                nickname="myfavor",
                age=15,
                )
            token = default_token_generator.make_token(user)
            email_vertification_token = EmailVerificationToken.objects.create(
                user=user,
                token=token,
            )
            auth_url=request.build_absolute_uri(reverse('email_verification', kwargs={'uidb64': uid, 'token': token}))
            
            subject="Account Activation"
            message = render_to_string('email_verify.html', {'auth_url': auth_url})
            plain_message = strip_tags(message)
            # Send email
            send_mail(
                subject,
                plain_message,
                "myfavor86@gmail.com",
                [user.email],
                html_message=message,
                fail_silently=False,
            )
            user.is_active=False#아직 이메일 인증을 하지 않음.
            user.save()
            return Response({"message":"해당 이메일 주소로 인증링크 전송 완료!"}, status=status.HTTP_200_OK)
       

step1_SignUP 클래스

get(): 사용자에게 이메일을 입력하라는 메시지를 반환한다.
post(): 사용자로부터 이메일을 받아 해당 이메일이 데이터베이스에 이미 존재하는지 확인한다. 만약 존재하지 않으면 새로운 사용자를 생성하고 해당 사용자에게 이메일 인증 링크를 전송한다. 이때 user의 is_active field는 False로 유지한다. (인증 링크는 auth_url 변수에 저장되어 있음)


class step2_SignUp(APIView):
    def get(self, request, pk, token):
        try:
            user = User.objects.get(pk=pk)
        except User.DoesNotExist:
            raise NotFound("User not found.")
        if not user.is_active:
            user.is_active=True
            user.save()
            return Response({"message": "E-mail 인증을 완료."}, status=status.HTTP_200_OK)
        else:
            return Response({"message": "이미 인증한 E-mail 입니다."}, status=status.HTTP_400_BAD_REQUEST)
    
    def post(self, request, pk, token):
        try:
            user = User.objects.get(pk=pk)
            print("user", user)
            if not user.is_active:
                return Response({"detail": "E-mail 인증을 완료해 주세요."}, status=status.HTTP_403_FORBIDDEN)
        
            password = request.data.get("password")
            if not password:
                raise ParseError
    
            serializer = PrivateUserSerializer(
                user,
                data=request.data,
                partial=True 
            )

            if serializer.is_valid():
                user = serializer.save()
                user.set_password(password)
                serializer = PrivateUserSerializer(user)
                pick=request.data.get("pick")
                print(pick)
                if pick:
                    try:
                        picked_idol=Idol.objects.get(pk=pick)
                        user.pick=picked_idol
                        user.save()
                        picked_idol.pickCount+=1
                        picked_idol.save()
                        user.is_active=True
                        user.save()
                    except Idol.DoesNotExist:
                        return Response({"error":"Pick한 아이돌이 없습니다!"}, status=status.HTTP_400_BAD_REQUEST)
                    return Response(serializer.data, status=status.HTTP_201_CREATED)
                else:
                    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        except User.DoesNotExist:
            raise NotFound("User not found.")

step2_SignUp 클래스

get(): 사용자의 이메일 인증을 완료한다. 사용자가 이미 인증되었다면, 메시지를 반환하여 이미 인증이 완료되었다고 알린다.

post(): 이 메소드는 사용자로부터 비밀번호를 받아 사용자의 비밀번호를 설정할 수 있도록 한다. 또한 사용자가 특정 아이돌을 선택하면, 해당 아이돌의 선택 카운트를 증가시킨다.(=남은 회원 가입 절차를 완료 할 수 있도록 함)


class EmailVerification(APIView):
    def get(self, request, pk, token):
        try:
            user = User.objects.get(pk=pk)
        except User.DoesNotExist:
            raise NotFound("User not found.")
        print("3", user,token)
        if not user.is_active:
            return HttpResponseRedirect(reverse("email_signUp_step2", args=[pk, token]))
        else:
            return Response({"detail": "Invalid verification link."}, status=status.HTTP_400_BAD_REQUEST)

EmailVerification 클래스

get(): 회원가입을 시도하는 사용자의 인증링크를 생성한다.


class Login(APIView):  #is_active 검사 
    def post(self, request, format=None):
        email = request.data.get("email")
        password = request.data.get("password")

        if not email or not password:
            raise ParseError("잘못된 정보를 입력하였습니다.")
        try:
            user = User.objects.get(email=email)
            print("login try user: ", user)
            if not user.is_active:
                raise ParseError({"error":"Email 인증을 완료해 주세요!"})
            
            user = authenticate(
            request,
            email = email,
            password = password,
            )
            if not user:
                raise AuthenticationFailed({"error": "이메일 또는 비밀번호가 올바르지 않습니다."})

            print("login try user, email vertify completed.: ", user)    
            
        except User.DoesNotExist:
            raise NotFound
                
        if user.check_password(password):
            login(request, user)
            return Response(status=status.HTTP_200_OK)
        else:
            return Response({"error": "비밀번호가 잘못되었습니다."}, status=status.HTTP_400_BAD_REQUEST)

Login 클래스

post(): 사용자로부터 이메일과 비밀번호를 받아 사용자를 인증하며. 사용자가 성공적으로 인증되면 로그인된다. 만약 이메일이 인증되지 않았다면, 에러 메시지를 반환한다.

profile
아악! 뜨거워!!

0개의 댓글