본문 바로가기

django/정리

[Django] NHN Cloud 휴대폰 sms 본인 인증 기능

 

먼저, 본인 인증에는 여러가지 방식이 있다.

대부분의 서비스에서 사용되는 휴대폰 본인 인증 방식(통신사 인증, 카카오톡 인증, 나이스 등...)은 사업자 등록이 필요하다. 사용자의 개인정보를 다루기 때문!

그만큼 강력한 보안을 보여주지만, 작은 프로젝트에서는 사용하기 힘들다.

가장 많이 보이는 화면

이번 프로젝트에서는 사업자 등록이 필요없는 단순한 문자 API를 활용하여 전화번호를 이용한 본인 인증을 이용하기로 했다.

휴대폰인 본인 소유인지 확인할 수는 없지만, 빠른 본인 인증이 가능하다.

SMS API를 제공하는 서비스도 다양하게 있다. 대표적으로 Twilio, Naver Cloud, AWS SNS 등을 많이 사용한다.

이번에 내가 하게 된 건 NHN Cloud 서비스! 선택지가 없었따...이전 개발자가 남긴 흔적

https://www.nhncloud.com/kr/service/notification/sms

대충 한 건에 10원 정도

 


#1. 개발 전 셋팅

https://www.nhncloud.com/kr

 

NHN Cloud : 유연하게 안전하게 비즈니스의 힘이 되는 통합 클라우드 서비스

안정적이고 유연한 기업용 클라우드 컴퓨팅 서비스, 오픈스택 기반의 개방성과 신뢰로 고객사의 비즈니스에 힘이 되는 NHN Cloud

www.nhncloud.com

위 사이트에서 회원가입 후 프로젝트 등록을 해주어야 한다.

프로젝트를 등록하면서 문자를 보낼 전화번호를 등록하고, 필요한 값을 가져온다.

필요한 값은 appKey, X-secret-key이다.

자세한 과정은 아래를 참고하기. 나는 기존에 연결해둔 걸 이용해 인증 API만 구현했다.

https://velog.io/@yukina1418/%ED%95%B8%EB%93%9C%ED%8F%B0-%EB%B3%B8%EC%9D%B8%EC%9D%B8%EC%A6%9D-NHN-Cloud%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EA%B8%B0#%ED%9C%B4%EB%8C%80%ED%8F%B0-%EB%B3%B8%EC%9D%B8-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%B4%EB%B3%B4%EA%B8%B0-with-nhn-cloud

 

핸드폰 본인인증, NHN Cloud로 만들어보기.

간단하게 본인인증 기능 구현해보기

velog.io

 

 


#2. SMS 인증 API 개발

# models.py

class SmsAuth(TimeStampedModel):
    phone_num = models.CharField(max_length=32)	
    is_authenticated = models.IntegerField(default=0)
    auth_code = models.CharField(max_length=8, blank=True, null=True) 
    send_result = models.IntegerField(default=0) # 전송 완료 여부
    
    def __str__(self):
        return f"{self.phone_num}"

user랑 별개로 sms인증 데이터 저장을 위한 모델을 만들었다.

auth_code에 인증을 위한 랜덤 번호가 담길 것이다.

 

# views.py

class SendSmsAPIView(APIView):
    renderer_classes = [CustomRenderer]

    @send_sms_post_schema
    def post(self, request):
        phone_num = request.data.get('phone_num')
        if not phone_num:
            return Response({'error': 'Phone number is required'}, status=status.HTTP_400_BAD_REQUEST)
        phone_num = phone_num.replace('-', '')

        # 인증 코드 생성
        auth_code = ''.join(random.choices(string.digits, k=6))
        
        # SMS 전송
        url = f"{settings.SMS_CLIENT_END_POINT}/sms/v3.0/appKeys/{settings.SMS_CLIENT_APP_KEY}/sender/sms"
        headers = {
            'Content-Type': 'application/json;charset=UTF-8',
            'X-Secret-Key': f'{settings.SMS_CLIENT_SECRET_KEY}'
        }
        payload = {
            "body": f"[회사명^^] 인증번호 {auth_code}를 입력해주세요.",
            "sendNo": f'{settings.SMS_CLIENT_SEND_NO}',
            "recipientList": [{"recipientNo": phone_num}]  # 하이픈 상관 없음. 단, 인증할 때랑은 같아야 함.
        }
        
        response = requests.post(url, json=payload, headers=headers)

        # print("Response Status Code:", response.status_code)
        # print("Response Body:", response.text)

        
        if response.status_code == 200:
            # SMS 전송 결과 저장
            SmsAuth.objects.create(
                phone_num=phone_num,
                auth_code=auth_code,
                send_result=1,  # 1 indicates success
                ip_address=request.META.get('REMOTE_ADDR')  # ip address 저장하기
            )

            return Response({'message': '인증 코드 발송'}, status=status.HTTP_200_OK)
        else:
            return Response({'error': '인증 코드 발송 실패'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

인증 번호 전송하기. https://docs.nhncloud.com/ko/Notification/SMS/ko/api-guide/ 공식문서는 꼭 읽기~

url은 https://api-sms.cloud.toast.com 이고, 위와 같이 전송하면 된다.

중요한 정보는 따로 관리하자.

특히 중요한 전화번호는 '-'를 빼고 통일된 형식으로 저장되도록 했다.

꼭 넣어주기

request body에는 여러 값을 넣을 수 있는데, 나는 단순하게 필수값인 bodu(문자 내용), sendNo(발신번호)와 recipientList[].recipientNo(수신번호) 을 넣어주었다. 

다른값을 쓸 일이 있을라나

 

 

class VerifyAuthCodeAPIView(APIView):
    renderer_classes = [CustomRenderer]

    @verify_auth_code_post_schema
    def post(self, request, *args, **kwargs):
        phone_num = request.data.get('phone_num')
        auth_code = request.data.get('auth_code')
        phone_num = phone_num.replace('-', '')

        try:
            sms_auth = SmsAuth.objects.get(phone_num=phone_num, auth_code=auth_code)
        except SmsAuth.DoesNotExist:
            return Response({'error': '인증 코드가 틀렸습니다.'}, status=status.HTTP_400_BAD_REQUEST)

        # 5분 시간 제한 확인
        time_limit = sms_auth.created_at + timedelta(minutes=5)
        if timezone.now() > time_limit:
            return Response({'error': '인증 시간이 만료되었습니다.'}, status=status.HTTP_400_BAD_REQUEST)
        sms_auth.is_authenticated = 1
        sms_auth.save()
        return Response({'message': '인증이 완료되었습니다.','data': SmsAuthSerializer(sms_auth).data}, status=status.HTTP_200_OK)

5분의 제한 시간을 두고, phone_num과 auth_code를 이용해 SmsAuth 객체를 비교해 인증을 수행한다.

인증을 수행한 객체는 다시 사용하지 않도록 is_authenticated값을 1로 만들기

 

성공~

 


#3. 회원가입 API 개발

모델은 원하는 아무 user모델을 사용해도 상관없다.

중요한건 위에서 sms 인증 후 얻은 smsauth 객체의 id를 이용해서 인증을 수행하는 것이다.

  renderer_classes = [CustomRenderer]

    @designer_profile_post_schema
    def post(self, request):
        user_data = request.data.get('user')
        designer_profile_data = request.data.get('designer_profile')
        phone_num = request.data.get('phone_num').replace('-', '')
        auth_id = request.data.get('auth_id')  # 인증을 위한 request field!!!!

        # 인증 정보 및 전화번호를 미리 조회하여 효율적으로 처리
        existing_user = User.objects.filter(phone=phone_num).first()
        auth_info = SmsAuth.objects.filter(id=auth_id, is_authenticated=1).first()

        if existing_user:
            return Response({"error": "이미 인증된 전화번호 입니다."}, status=status.HTTP_400_BAD_REQUEST)
        
        if not auth_info:
            return Response({"error": "인증 정보가 정확하지 않습니다."}, status=status.HTTP_400_BAD_REQUEST)

        # 비밀번호 검증
        if 'password1' in user_data and 'password2' in user_data:
            if user_data['password1'] != user_data['password2']:
                return Response({"error": "비밀번호가 일치하지 않습니다."}, status=status.HTTP_400_BAD_REQUEST)

        # 'password2' 필드를 제거
        password = user_data.pop('password1', None)
        user_data.pop('password2', None)  # password2 필드를 제거합니다.

        # UserSerializer로 사용자 생성
        user_serializer = UserSignUpSerializer(data=user_data)

        if user_serializer.is_valid():
            email = user_serializer.validated_data.get('email')
            user_data = user_serializer.validated_data.copy()

            if User.objects.filter(email=email, is_deleted=0).exists():
                return Response({"error": "이미 존재하는 사용자입니다."}, status=status.HTTP_400_BAD_REQUEST)

            # User 객체 생성 및 비밀번호 설정
            user_data['login_id'] = email
            user = User(**user_data)
            if password:
                user.set_password(password)
            user.phone = phone_num
            user.is_password_migrated = 1
            user.save()

        else:
            return Response({"error": "사용자 데이터가 유효하지 않습니다.", "message": user_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

        # DesignerProfile 생성
        designer_profile_data['user'] = user.id
        designer_profile_serializer = DesignerSignUpSerializer(data=designer_profile_data)

        if designer_profile_serializer.is_valid():
            designer_profile_instance = designer_profile_serializer.save()
            return Response({"message": "작가 프로필이 성공적으로 생성되었습니다.", "data": DesignerSignUpSerializer(designer_profile_instance).data}, status=status.HTTP_201_CREATED)
        
        # 작가 프로필 생성 실패 시 사용자 삭제
        user.delete()
        return Response({"error": "작가 프로필 데이터가 유효하지 않습니다.", "message": designer_profile_serializer.errors}, status=status.HTTP_400_BAD_REQUEST)

다른건 볼 필요 없구, auth_id field를 통해 받은 id값으로 해당 인증 기록이 존재하는지 찾는다. 

auth_info = SmsAuth.objects.filter(id=auth_id, is_authenticated=1).first() 이 줄만 추가하면 간단하게 인증을 할 수 있다.

이 회원가입 로직에는 email 비교와 password 검증을 위해 2번 입력받는 것까지 포함했다.

 

 


# 회고

 

프로젝트를 진행하면서 새롭게 알게된 것들이 많다.

이제 슬슬..백엔드 개발이 마무리 되어가면서 정리를 좀 해야겠다.