본문 바로가기

개발이야기

방슐랭가이드에 핸드폰 인증 구현하기

방슐랭 가이드 정식 버전을 개발하면서 핸드폰 인증 부분을 개발하게되었다.

우선 검색을 통해서 네이버 SENS( Simple & Easy Notification Service)라는 API를 활용하기로 결정했다. 

 

SENS에서 프로젝트를 하나 만들어 준다.

 

 

 

나는 bclguide라는 이름의 프로젝트를 만들었다. 프로젝트를 만든 후 발신 번호를 등록해야한다. 본인 소유의 핸드폰을 증명하기 위해 서류가 필요하다.

 

 

 

 

kt를 이용한다면 케이티 엠모바일에서 발급 받을 수 있다. 위 서류를 제출하면 1~2일 기다려야한다.

 

 

 

기다리면 네이버에서 위와 같이 서류 인증을 해준다. 방슐랭가이드의 경우에는 하루 걸렸다.

네이버 SENS( Simple & Easy Notification Service)의 정말 좋은 점은 정말 쉬운 사용법인것 같다. 아래의 폼만 입력한다면 정말 쉽게 문자를 보낼 수 있다. 특히 CSV파일도 올릴 수 있어서 대용량으로 문자를 보낼 수 있다.

 

 

코드 이야기를 해보자. 우선 방슐랭 서버는 Node.js 로 만들어졌고, 언어는 TypeScript를 사용하고 있다.

SENS API를 사용하기 위해 아래 패키지를 설치해주자.

 

yarn add crypto-js
yarn add -D @types/crypto-js

 

crypto-js는 javascript를 이용한 서비스에서 여러가지 정보(ex: 비밀번호)를 안전하게 암호화 할 수 있게 해준다. crypto-js의 암호화 방식은 다음과 같다.

 

  1. 대칭키(Symmetric Encryption): 암호화-복호화할 때 같은 키 값을 이용
  2. 비대칭키(Asymmetric Encryption):암호화 복호화 할 때 다른 키 값을 이용
  3. 해싱(hashing): 단방향으로 암호화만 가능하고 복호화 할 수 없다. 비밀번호 등에 이용

 

우리는 HMAC(Hash based Message Authentication Code) 이라는 것을 활용한다. 해시 암호키를 송신자와 수신자가 미리 나눠가지고 사용하고, 공유하고 있는 키와 원본 메시지를 혼합하여 해시값을 만들고 이를 비교하여 무결성을 확인한다고 한다.

SENS API를 활용해서 핸드폰에 문자를 보내는 코드는 아래와 같다.

 

import SendMessageError from '@/errors/sendMessageError';
import axios from 'axios';
import CryptoJS from 'crypto-js';

import config from '../config';

const makeSignature = () => {
  const NCP_SERVICE_ID = config.ncp.service_id;
  const NCP_URL2 = `/sms/v2/services/${NCP_SERVICE_ID}/messages`;
  const NCP_SECRET_KEY = config.ncp.secret_key;
  const NCP_ACCESS_KEY = config.ncp.access_key;
  const method = 'POST';
  const space = ' ';
  const newLine = '\n';
  const timestamp = Date.now().toString();

  const hmac = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, NCP_SECRET_KEY!);

  hmac.update(method);
  hmac.update(space);
  hmac.update(NCP_URL2);
  hmac.update(newLine);
  hmac.update(timestamp);
  hmac.update(newLine);
  hmac.update(NCP_ACCESS_KEY!);

  const hash = hmac.finalize();
  const signature = hash.toString(CryptoJS.enc.Base64);

  return signature;
};

const sendSMS = async (phoneNumber: string) => {
  const NCP_SERVICE_ID = config.ncp.service_id!;
  const NCP_URL = `https://sens.apigw.ntruss.com/sms/v2/services/${NCP_SERVICE_ID}/messages`;

  const body = {
    type: 'SMS',
    contentType: 'COMM',
    countryCode: '82',
    from: '01012345678', // sens api 에 등록된 번호
    content: '문자내용',
    messages: [
      {
        to: phoneNumber,
      },
    ],
  };

  const options = {
    headers: {
      'Content-Type': 'application/json; charset=utf-8',
      'x-ncp-iam-access-key': config.ncp.access_key!,
      'x-ncp-apigw-timestamp': Date.now().toString(),
      'x-ncp-apigw-signature-v2': makeSignature(),
    },
  };

  try {
    const { data } = await axios.post(NCP_URL, body, options);
    return data;
  } catch (error: any) {
    throw new SendMessageError(error.message);
  }
};

export default sendSMS;

 

이제 우리는 문자를 보낼 수 있게 되었지만, 인증번호는 보내기만 하는게 아니고 서버에서 사용자의 핸드폰 번호에 해당하는 인증번호를 알고 있어야한다. (기억하고 있어야한다.)

 

다음은 인증번호의 특징이다.

 

- 인증번호는 저장의 대상이지만, 영구적인 대상이 아니다.

- 인증번호는 재요청에 의해 변경될 수 있다.

- 인증번호는 핸드폰 번호에 1:1 관계를 갖는다.

 

인증번호는 재전송이 가능해야하기 때문에 인증번호에 대한 빈번한 업데이트를 지원해야하며, 빈번한 업데이트로 인한 부담/부하가 적은 편이 좋다. → In memory Cache 또는 Redis

Cache를 사용한다면 각 요청에 대한 세션 처리가 추가로 이루어져야한다. 그렇게 될 경우 인증번호 요청과 검증 로직에 너무 큰 리소스가 사용된다. 따라서 Redis가 필요하다.

 

우선 방슐랭서버에 Redis를 설치해보자.

 

yarn add redis

 

설치한후 loaders 폴더에서 redis.ts 파일을 만들고 아래 내용을 적어준다. (loaders 폴더는 express 미들웨어를 로딩하거나, mysql과 연동하는등 서버를 시작하기전에 로딩해야하는 것들을 적어놓는다.)

 

문자 인증의 flow를 생각해보자.

 

1. 사용자가 연락처를 입력하고 인증번호 받기를 누른다.

2. 서버에 사용자의 연락처와 난수를 생성해서 저장한다.

3. 난수는 사용자에게 보내준다.

4. 사용자는 문자로 받은 난수를 입력하고 확인버튼을 누른다.

5. 서버에서 일치 여부를 판단하고 응답을 보낸다.

6. 사용자가 재전송 버튼을 누르면 -> 서버에서 다시 2번프로세스부터 진행한다.

 

클라이언트 사이드에서 인증번호 받기를 누르면 버튼은 두가지로 변한다.

첫번째는 인증번호 인증하기, 두번째는 재전송하기이다. 기능 구현을 우선으로 일단 공부한 내용을 바탕으로 redis에 읽기 쓰기 작업을 진행했다. setEx 함수를 이용해서 3분동안 인증번호를 저장하고 3분이 지나면 Expire 처리한다.

import { Request, Response } from 'express';

import createRandomNumber from '@utils/createRandomNumber';
import redisClient from '@loaders/redis';
import { logger } from '@/utils/logger';
import sendSMS from '@/utils/sendMessage';

const DEFAULT_EXPIRATION = 60 * 3;

export const sendCertificationNumber = async (req: Request, res: Response) => {
  const { phoneNumber } = req.body;

  if (phoneNumber.length !== 11) {
    return res.status(400).json({ message: '핸드폰 번호가 아닙니다.' });
  }

  const certificationNumber = createRandomNumber(6);

  try {
    redisClient.setEx(phoneNumber, DEFAULT_EXPIRATION, certificationNumber);
  } catch (error) {
    return res.status(500).json({ message: '서버 오류입니다.' });
  }

  sendSMS(phoneNumber, certificationNumber);
  return res.status(200).json({ message: '인증번호가 발송되었습니다.' });
};

export const resendCertificationNumber = async (req: Request, res: Response) => {
  const { phoneNumber } = req.body;

  if (phoneNumber.length !== 11) {
    return res.status(400).json({ message: '핸드폰 번호가 아닙니다.' });
  }

  const certificationNumber = createRandomNumber(6);

  try {
    redisClient.setEx(phoneNumber, DEFAULT_EXPIRATION, certificationNumber);
  } catch (error) {
    return res.status(500).json({ message: '서버 오류입니다.' });
  }

  sendSMS(phoneNumber, certificationNumber);
  return res.status(200).json({ message: '인증번호가 발송되었습니다.' });
};

export const checkCertificationNumber = async (req: Request, res: Response) => {
  const { phoneNumber, certificationNumber } = req.body;

  try {
    const storedCertificationNumber = await redisClient.get(phoneNumber);
    if (storedCertificationNumber === certificationNumber) {
      return res.status(200).json({ message: '인증에 성공하였습니다.' });
    }
  } catch (error) {
    logger.error(error);
  }

  return res.status(400).json({ message: '인증번호가 일치하지 않습니다.' });
};

 

난수를 생성하는 코드는 아래와 같이 작성하였다.

 

const createRandomNumber = (len: number) => {
  let result = '';

  for (let i = 0; i < len; i++) {
    result += parseInt(String(Math.random() * 10));
  }

  return result;
};

export default createRandomNumber;

 

이렇게까지 방슐랭 가이드에 핸드폰 인증 서비스를 구현해보았다.

'개발이야기' 카테고리의 다른 글

Agile Development Principle  (0) 2022.05.17
Component Driven Development (CDD)  (0) 2022.05.13
qwzd에 eslint, prettier 적용하기  (0) 2022.05.12
Cypress에서 브라우저의 권한을 다루는 방법  (0) 2022.04.29
Next.js PWA 찍먹  (0) 2022.04.26