[ PROJECT ] 당근마켓 클론코딩 - #03 SMS 인증하기

Hailee·2020년 12월 30일
2

[ PROJECT ]

목록 보기
13/16
post-thumbnail

네이버 클라우드 플랫폼의 SMS 인증 API를 사용한 기능 구현!

네이버의 친절한 공식 설명! 👉🏻 SMS API
실질적으로 내가 참고한 블로그! 👉🏻 Open API를 이용한 회원가입 문자인증

from django.shortcuts import render

# Create your views here.
import json, bcrypt, jwt, re
import sys
import os
import hashlib
import hmac
import base64
import requests
import time
from random import randint

from django.views               import View
from django.http                import JsonResponse, HttpResponse
from rest_framework             import status
from rest_framework.response    import Response
from rest_framework.views       import APIView
from django.core.exceptions     import ValidationError

#from user.utils         import login_decorator
from my_settings        import (service_id, secretKey, AUTH_ACCESS_KEY, AUTH_SECRET_KEY, SMS_SEND_PHONE_NUMBER, SECRET_KEY, ALGORITHM)
from user.models        import (
                                User,
                                AuthSms
                            )

# SMS 인증 (1) - 인증코드 발송
class SMSVerificationView(View):
    def send_verification(self, phone_number, auth_number):
        # 네이버 클라우드 플랫폼 주소 입력
        SMS_URL    = 'https://sens.apigw.ntruss.com/sms/v2/services/'+f'{service_id}'+'/messages'
        
        # time.time()*1000은 1970년 1월 1일 00:00:00 협정 세계시(UTC)부터의 경과 시간을 밀리초(Millisecond)단위로 나타낸 것
        ## (API Gateway 서버와 시간 차가 5분 이상 나는 경우 유효하지 않은 요청으로 처리하기 위해 필요)
        timestamp  = str(int(time.time()*1000))

        secret_key = bytes(AUTH_SECRET_KEY, 'utf-8')
        
        # 요청보내기 위한 준비
        method  = 'POST'
        uri     = '/sms/v2/services/'+f'{service_id}'+'/messages'
        message = method + ' ' + uri + '\n' + timestamp + '\n' + AUTH_ACCESS_KEY
     
        # 암호화를 위해 bytes type으로 인코딩
        message = bytes(message, 'utf-8')

        # 위에서 생성한 StringToSign(message)를 HmacSHA256 알고리즘으로 암호화한 후
        # base64로 인코딩해서 signingKey 생성
        signingKey = base64.b64encode(
                        hmac.new(secret_key, message, digestmod=hashlib.sha256).digest()
                    )
        
        # 요청 헤더
        headers    = {
            'Content-Type'             : 'application/json; charset=utf-8',
            'x-ncp-apigw-timestamp'    : timestamp,
            'x-ncp-iam-access-key'     : AUTH_ACCESS_KEY,
            'x-ncp-apigw-signature-v2' : signingKey,
        }

        # 요청 바디
        body       = {
            'type'        : 'SMS',
            'contentType' : 'COMM',
            'countryCode' : '82',
            'from'        : f'{SMS_SEND_PHONE_NUMBER}',
            'content'     : f'키위마켓입니당>< \n인증번호 [{auth_number}]를 입력해주세요.',
            'messages'    : [
                {
                    'to' : phone_number
                }
            ]
        }

        ## 만든 바디를 json 형태로 변환한 뒤
        encoded_data = json.dumps(body)
        # 헤더와 함께 post 메소드로 SMS 전송 url에 요청을 보낸다.
        res          = requests.post(SMS_URL, headers=headers, data=encoded_data)
        return HttpResponse(res.status_code)

    def post(self, request):
        try:
            data                = json.loads(request.body)
            phone_number        = data['phone_number']

            # 인증번호 랜덤 생성
            auth_number = str(randint(100000, 999999))
            
            # 휴대전화 번호가 존재하는지 여부 확인한 뒤 작업.
            AuthSms.objects.update_or_create(
                phone_number = phone_number,
                defaults     = {
                    'phone_number': phone_number,
                    'auth_number' : auth_number
                }
            )

            # 휴대전화번호와 인증번호를 담아 같은 클래스 내 send_verification 호출
            self.send_verification(
                phone_number = phone_number,
                auth_number = auth_number
            )
            return HttpResponse(status=200)
        except KeyError:
            return JsonResponse({'message' : 'Invalide key'}, status=400)

# SMS 인증 (2) - 인증코드 인증 후 삭제
class VerificationCodeView(View):
    def post(self, request):
        try:
            data = json.loads(request.body)
            if AuthSms.objects.filter(phone_number=data['phone_number']).exists():
                code = AuthSms.objects.get(phone_number = data['phone_number'])
                if code.auth_number == int(data['auth_number']):
                    code.delete()
                    return JsonResponse({'code': 'ACCEPT'}, status=200)
            return JsonResponse({'message': 'DENY'}, status=400)   
                 
        except KeyError:
            return JsonResponse({'message': 'KEY_ERROR'}, status=400)

아직 멘토님께 첨삭받지 않은, 저 블로그를 참고한 첫 로직이다.

정말, 이 흐름을 이해하느라 너무 오래걸렸다.

난 원래가 이런 API를 가져다가 사용하는데에 몹시 취약한 사람이다.
국비시절 이메일 보내기 API(SMTP)도 정말.. 공식 설명을 보고 또 봐도 이해하지 못해서 너무 슬펐던 기억이 있다.

다른 사람들은 수월하게 가져다가 쓰는 것 같은데 나는 왜 이렇게 힘들어야하는가.. 머리가 돌인가.... HOXY.... 이런 느낌..?

여러 블로그들, 공식 설명을 참조해도 이해가 갈듯, 말듯 정말 머리아픈 상태에서 화요일 밤을 보내고 잠들었는데
오늘 드디어 저 단비같은 블로그를 발견해서!! 해결할 수 있었다.


우선 단순 post 메서드를 통해 핸드폰 번호를 입력받은 뒤, djangoupdate or create 메서드를 사용해서 테이블 내 해당 데이터가 있는지 조회한다.

이건 마치 Oraclemerge into같은 늑낌

있으면 번호만 교체, 없으면 데이터를 추가한다.
이렇게 해주는 이유는 같은 번호로 여러번 인증을 시도할 수 있기 때문!

사실 모든 인증이 끝나면 이 유저의 데이터는 테이블에서 삭제할 것이지만,
어쨌든 인증을 시도하는 순간에서 여러번의 시도를 할 수도 있기 때문이다.


네이버 API를 사용하기 위해 가입한 정보가 나의 정보가 아니기 때문에 request 실행 내역은 볼 수 없었지만
(같은 ip에서는 한명만 가입 가능하다고한다 ^^ 먼저 가입한 팀원의 계정으로 개발을 진행했다. 네이버.. 부들부들.. )

  1. post메서드를 통해 DB에 입력받은 번호와 랜덤한 번호를 저장한 뒤
  2. 입력받은 번호를 가지고 send_verification 메서드를 실행한 뒤,
  3. header, body 양식대로 네이버 클라우드 플랫폼으로 암호화한 정보를 가지고 요청을 보낸 뒤
  4. 문자가 오기를 기다리면 끗!

정말 간단했는데.. 이 흐름 자체가 이해되지를 않으니
완성된 남의 코드를 보아도 이해를 할 수가 없었다.

내일 멘토님의 살벌한 리뷰.. 기다려보쟈

profile
웹 개발 🐷😎👊🏻🔥

1개의 댓글

comment-user-thumbnail
2024년 1월 31일

안녕하세요 도움좀 받을수 있을까요?

답글 달기