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

·2022년 7월 25일
13

SKILL

목록 보기
10/16
post-thumbnail

내일 뭔가 기대가 많이 되는 날이라 그런가 집중이 잘 안된다...ㅋㅋ
그래서 그냥 가볍게 쓸 수 있는? 그런 기능이나 하나 적어보려고 한다.

휴대폰 본인 인증 기능 구현해보기 with NHN Cloud

우리가 일반적으로 회원가입을 할 때는 pass라던가 여러가지 솔루션을 통해서 핸드폰 본인인증 과정이 진행된다.
그렇지만 그러한 솔루션은 사업자 등록증이 있어야하기에, 일반적으로 개인 프로젝트에서 도입하는 것은 불가능하다.

근데 난 넣고 싶어!!!!!!!!

라고 한다면, NHN Cloud를 이용하여 구현을 할 수 있다.

하지만 신용카드가 없다면, 해당 서비스를 사용할 수 없기에 없다면 누군가에게 부탁하자.

메세지는 한건당 대충 10원정도 나간다.

사전 준비 사항

일단 회원가입부터 하자

https://www.toast.com/kr

콘솔 대시보드를 들어간다.

서비스 선택

Email 인증이 아니라 핸드폰 본인 인증을 할꺼니까, SNS를 누르자.

결제수단을 알아서 등록하고, 발신 번호 변작 금지 동의를 누른 후 확인을 누르자.

제일 먼저 해야하는 것은, 어떤 녀석의 번호로 보내는 것인지를 정해줘야 한다. 발신 번호 관리를 눌러보자.

알아서 인증하면 아래처럼 등록 완료가 된 것을 볼 수 있다.

여기서 한가지 문제가 있다.

저기에 등록하는 발신번호로 실제로 문자를 보내질 수 있기에 자신의 개인정보가 노출 될 수 있는 위험성이 존재하는데
이 부분에 대해서는 각 통신사에 존재하는 투 폰 시스템을 사용하여 두 개의 번호를 만들어 쓰는 것을 권하고 있다.
한달에 사천원인가? 생각보다 비싸진 않다.

메세지를 보내보자!

이젠 정말로 메세지를 보내기 위한 작업을 해보자!

언제나 새로운 라이브러리(?)를 사용할 때는 공식 가이드를 보고 하도록 하자.

제일 최신 버전인 API v3.0 가이드를 보자.

아! 대충 뭔가 보이는 것 같다.

일단 요청하는 엔드포인트(URL)은
POST 메소드 https://api-sms.cloud.toast.com/sms/v3.0/appKeys/{appKey}/sender/sms

이렇게 보내면 되는 것 같은데, 이것만으로는 보낼 수 없으니 헤더와 바디도 확인을 해보자.

  • 헤더
    • X-Secret-Key : 고유의 시크릿 키
  • 바디
    • body : 보낼 내용
    • sendNo : 발신자 번호 (보내는 사람)
    • recipientList[].internationalRecipientNo : 국가 번호가 포함된 수신자 번호 (받는 사람)

그렇지만 아직 appKey, X-Secret-Key 같은 것은 우린 획득(?)하지 못했다.
어디서 발급을 받을 수 있는지 확인해보자.

appKey, XSecretKey 발급하기

가이드를 끄고, 뒤로 돌아와서 가이드 옆에 있던 URL / Appkey 버튼을 누를 경우 확인을 할 수 있다.

여기서 잠깐!

다양한 사이트에서 제공하는 API를 사용하는 경우, 이와같이 AppKey와 SecretKey를 발급받아야하는 경우가 있다.
이것은 절대로 누군가가 보면 안되기에 세심한 관리가 필요하다.

2022년 5월 AWS의 시크릿키를 깃허브에 노출하여, 3억가량의 비용이 청구된 일이 있었다.

원본의 글은 사라졌는데, 해당 내용으로 찾아보면 글의 전문을 읽어볼 수 있다.

AWS 해킹으로 3억청구당한 개발자ㄷ(웃대펌)
GCP를..... 해킹당했습니다....?
(저도 과거에 아무 생각없이 올렸다가 GCP 계정 정지먹음)

그러니 절대로 깃허브 저장소에는 올리지 않고, 환경변수로 관리를 하는 것을 추천한다.
.env
.gitignore 를 활용하자!!

그렇게 작성된 나의 코드

Nest의 HttpService 버전

    await this.httpService
      .post(
        `https://api-sms.cloud.toast.com/sms/v3.0/appKeys/${process.env.SMS_APP_KEY}/sender/sms`,
        {
          body: `안녕하세요 인증번호는 [${token}]입니다.`,
          sendNo: process.env.SMS_SENDER,
          recipientList: [
            {
              internationalRecipientNo: phone,
            },
          ],
        },
        {
          headers: {
            'Content-Type': 'application/json;charset=UTF-8',
            'X-Secret-Key': process.env.SMS_X_SECRET_KEY,
          },
        },
      )
      .toPromise();

나같은 경우에는 Nest의 HttpService를 사용해서 조금 형식이 다를 수 있긴 하지만, 기본적으로 axios를 사용할 때와 크게 다르지 않다.

Axios 버전

  const result = await axios.post(
    `https://api-sms.cloud.toast.com/sms/v3.0/appKeys/${appKey}/sender/sms`,
    {
      body: `안녕하세요 인증번호는 [${mytoken}]입니다.`,
      sendNo: sender,
      recipientList: [
        {
          internationalRecipientNo: myphone,
        },
      ],
    },
    {
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
        "X-Secret-Key": XSecretKey,
      },
    }
  );

(뭐야 다른게 그냥 없네)

그렇게 저 API를 아래처럼 호출을 할 경우에

 recipientList: [
        {
          internationalRecipientNo: 자기 번호,
        },

한번 누를 때 마다 10원씩 빠지는 것을 확인할 수 있다(ㅋㅋ)

뭐야 랜덤 6자리 번호는 어떻게 만들어요?

의문이 들 수도 있다. 안든다고? 그럼 당신은 천재인 것 같다. 난 저거 어케만드냐고 궁금했는데

JS에는 내장객체라는 것이 있는데, 거기서 수학에 관련된 것을 모아놓은 Math라는 것이 있다.

자세한 것은 알아서 찾아보시고(...)

몇가지만 알면 된다.

  • Math.random
    • 난수를 생성해준다. 대신, 0을 넘기지 못한다.
  • Math.floor
    • 버림을 한다.

그래서 두개 메소드로 혼합하여 6자리 난수를 만들 수 있다.

그렇지만, 여기서 한가지 문제가 발생하는데
맨 앞의 숫자가 0일 경우에 자신이 원하는 자리수보다 1이 모자란 수를 획득할 수 있다.

그래서 String.prototype.padStart() 라는 메소드를 함께 사용하여 6자리 난수가 고정적으로 생성될 수 있도록 만든다.

  • padStart
    • 원하는 길이를 첫 번째 인자에 입력하고, 모자른 경우 채우는 값을 두 번째 인자에 입력한다.

이제 우리는 6자리 난수를 만들 수 있는 사람이 됐다!

3분동안 유지하는건 어떻게 해야하지? with Redis

보통 핸드폰 인증같은 기능을 보면 3분이 국룰이다.

게임의 OTP도 비슷한 구조로 되어있는데, 내 딴에는 시간을 기준으로 데이터 검증이 필요하다면 레디스를 활용하고 있다.

레디스가 무엇인지에 대하여는 과거에 작성해놓았던, 글에 링크를 걸어놓겠다.
Redis가 사랑받는 이유에 대하여

정말 필요한 요소만 이야기를 해보자면, 레디스를 사용할 경우 데이터가 DB에 존재하는 시간을 내가 정할 수 있다.

저러한 기능이 있기 때문에, 인증번호를 발급하고 3분동안 검증을 할 수 있는 로직을 만들 수 있다.

코드를 확인하면서, 자세하게 봐보자.

async sendTokenToPhone({ phone }) {
    // 6자리 난수(토큰)을 생성한다.
    const token = String(Math.floor(Math.random() * 10 ** 6)).padStart(6, '0');
    
    // 인증요청을 하는 번호로 토큰이 존재하는지 확인한다.
    const tokenData = await this.cacheManager.get(phone);
    
	// 해당 번호가 Key, 생성된 난수를 Value로 저장하고, TTL(데이터가 유효한 시간)을 180초, 3분으로 지정한다.
    await this.cacheManager.set(phone, token, { ttl: 180 });

	// 아래 post 요청을 통하여 해당 유저에게 위에 발급된 토큰을 보내준다.
    await this.httpService
      .post(
        `https://api-sms.cloud.toast.com/sms/v3.0/appKeys/${process.env.SMS_APP_KEY}/sender/sms`,
        {
          body: `안녕하세요 인증번호는 [${token}]입니다.`,
          sendNo: process.env.SMS_SENDER,
          recipientList: [
            {
              internationalRecipientNo: phone,
            },
          ],
        },
        {
          headers: {
            'Content-Type': 'application/json;charset=UTF-8',
            'X-Secret-Key': process.env.SMS_X_SECRET_KEY,
          },
        },
      )
      .toPromise();
    
    // 맨 처음 유저의 핸드폰 번호로 데이터를 찾았을 때, 없었다면 발송
    // 존재했다면 인증번호가 변경되었다는 문자열을 리턴한다.
    if (tokenData === null) {
      return `인증번호가 발송되었습니다.`;
    } else {
      return `인증번호가 변경되었습니다.`;
    }
  }

총 3번 눌러서 3개의 메세지를 받은 모습을 확인할 수 있다.

검증을 하는 것은 더더욱 간단하다.

 async checkToken({ phone, token }) {
 	// 번호를 키로 저장했기 때문에, 번호로 해당 데이터를 불러온다.
    const tokenData = await this.cacheManager.get(phone);
    
    // 두개가 동일할 경우 true, 다를 경우 false를 리턴하면 된다.
    if (tokenData === token) {
      return true;
    } else {
      return false;
    }
  }

하지만 이것으로 모든 것을 처리할 순 없고, 프론트단에서 한번 더 작업을 해주는 것이 좋다고 알고 있다.
왜냐하면 true라는 상태를 유지해야만 하는데

프론트쪽에서도 1차적으로 비밀번호의 자리수라던가 중복검증같은 행동이 완료가 된 후 회원가입이 가능하기에
해당 인증번호 검증 또한 프론트와의 협의가 필요하다.


나는 이것을 whiteList를 만들기 위해 사용하기도 했는데, 한번 방법을 알아두면 다양하게 응용을 할 수 있어서 좋았던 것 같다.

끝!

profile
물류 서비스 Backend Software Developer

1개의 댓글

comment-user-thumbnail
2023년 2월 9일

깔끔하고 상세한 정리 고맙습니다!

답글 달기