그저 내가 되었고

🎯Node.JS:: 네이버 클라우드 플랫폼; 본인 인증(SMS 인증) Rest API 구현하기 본문

개발/Node.js

🎯Node.JS:: 네이버 클라우드 플랫폼; 본인 인증(SMS 인증) Rest API 구현하기

hyuunii 2022. 12. 5. 22:54

 

Naver Simple & Easy Notification Service

(이하 SENSE)

+ SENSE?
별도의 메시지 서버 구축 없이
SMS, PUSH, 알림톡 등을 통해
메시지 알림 기능을 구현할 수 있는 서비스

 


1. 네이버 클라우드 플랫폼 접속하여 로그인 후 콘솔로 이동하여 서비스에서 Simple & Easy Notification Service(이하 SENSE) 클릭
https://www.ncloud.com

 

NAVER CLOUD PLATFORM

cloud computing services for corporations, IaaS, PaaS, SaaS, with Global region and Security Technology Certification

www.ncloud.com



2. 새 프로젝트 생성하기(SMS만 할거면 해당 박스만 체크)



3. 프로젝트 생성 후 서비스ID 복사해두기



4. 발신 번호(Calling Number) 등록하기


5. 마이페이지-계정관리-인증키 관리에서 인증키 생성 후 복사해두기
+ API인증키? API를 호출한 사용자가 권한을 가진 사용자인지 식별하는 도구



5. API 명세서 확인
메시지 발송 요청 URL

POST https://sens.apigw.ntruss.com/sms/v2/services/{serviceId}/messages

Content-Type: application/json; charset=utf-8
x-ncp-apigw-timestamp: {Timestamp}
x-ncp-iam-access-key: {Sub Account Access Key}
x-ncp-apigw-signature-v2: {API Gateway Signature}


API Header
항목Mandatory설명

항목 Mandatory 설명
Content-Type Mandatory 요청 Body Content Type을 application/json으로 지정 (POST)
x-ncp-apigw-timestamp Mandatory - 1970년 1월 1일 00:00:00 협정 세계시(UTC)부터의 경과 시간을 밀리초(Millisecond)로 나타냄
- API Gateway 서버와 시간 차가 5분 이상 나는 경우 유효하지 않은 요청으로 간주
x-ncp-iam-access-key Mandatory 포털 또는 Sub Account에서 발급받은 Access Key ID
x-ncp-apigw-signature-v2 Mandatory - 위 예제의 Body를 Access Key Id와 맵핑되는 SecretKey로 암호화한 서명
- HMAC 암호화 알고리즘은 HmacSHA256 사용


요청 Body

{
    "type":"(SMS | LMS | MMS)",
    "contentType":"(COMM | AD)",
    "countryCode":"string",
    "from":"string",
    "subject":"string",
    "content":"string",
    "messages":[
        {
            "to":"string",
            "subject":"string",
            "content":"string"
        }
    ],
    "files":[
        {
            "name":"string",
            "body":"string"
        }
    ],
    "reserveTime": "yyyy-MM-dd HH:mm",
    "reserveTimeZone": "string",
    "scheduleCode": "string"
}
 
 
항목 Mandatory Type 설명 비교
type Madantory String SMS Type SMS, LMS, MMS (소문자 가능)
contentType Optional String 메시지 Type - COMM: 일반메시지
- AD: 광고메시지
- default: COMM
countryCode Optional String 국가 번호 - SENS에서 제공하는 국가로의 발송만 가능
- default: 82
- 국제 SMS 발송 국가 목록
from Mandatory String 발신번호 사전 등록된 발신번호만 사용 가능
subject Optional String 기본 메시지 제목 LMS, MMS에서만 사용 가능
content Mandatory String 기본 메시지 내용 - SMS: 최대 80byte
- LMS, MMS: 최대 2000byte
messages Mandatory Object 메시지 정보 - 아래 항목 참조 (messages.XXX)
- 최대 100개
messages.to Mandatory String 수신번호 붙임표 ( - )를 제외한 숫자만 입력 가능
messages.subject Optional String 개별 메시지 제목 LMS, MMS에서만 사용 가능
messages.content Optional String 개별 메시지 내용 - SMS: 최대 80byte
- LMS, MMS: 최대 2000byte
files.name Optional String 파일 이름 - MMS에서만 사용 가능
- 공백 사용 불가
- *.jpg, *.jpeg 확장자를 가진 파일 이름
- 최대 40자
files.body Optional String 파일 바디 - MMS에서만 사용 가능
- 공백 사용 불가
*.jpg, *.jpeg 이미지를 Base64로 인코딩한 값
- 원 파일 기준 최대 300Kbyte
- 파일 명 최대 40자
- 해상도 최대 1500 * 1440
reserveTime Optional String 예약 일시 메시지 발송 예약 일시 (yyyy-MM-dd HH:mm)
reserveTimeZone Optional String 예약 일시 타임존 - 예약 일시 타임존 (기본: Asia/Seoul)
- 지원 타임존 목록
- TZ database name 값 사용
scheduleCode Optional String 스케줄 코드 등록하려는 스케줄 코드


응답 Body

{
    "requestId":"string",
    "requestTime":"string",
    "statusCode":"string",
    "statusName":"string"
}
 

 

항목 Mandatory Type 설명 비고
requestId Mandatory String 요청 아이디  
requestTime Mandatory DateTime 요청 시간  
statusCode Mandatory String 요청 상태 코드 - 202: 성공
- 그 외: 실패
- HTTP Status 규격을 따름
statusName Mandatory String 요청 상태명 - success: 성공
- fail: 실패


응답 Status

HTTP Status Desc
202 Accept (요청 완료)
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
429 Too Many Requests
500 Internal Server Error



6. 코드 작성
routes/sms.router.js

더보기
const express = require("express");
const router = express.Router();
const SMS = require('../controllers/sms');
const sms = new SMS();

// 회원가입 시 사용
router.post('/send', sms.send);
router.post('/verify', sms.verify);

// 아이디 찾을 때 사용
router.post('/sendID', sms.sendID);
router.post('/verifyID', sms.verifyID);

// 비밀번호 변경할때 사용
router.post('/sendPW', sms.sendPW);

module.exports = router;


controllers/sms.js

더보기
require("dotenv").config();
const axios = require('axios');
const CryptoJS = require('crypto-js');
const SmsService = require('../services/sms');

class SMS {
  smsService = new SmsService();

send = async(req, res, next) => {
  try {
    const phoneNumber = req.body.phoneNumber;
  
    const send = await this.smsService.send(phoneNumber);

    res.status(201).json({code: 201, message: "본인인증 문자 발송 성공", verifyCode: send})
  } catch(err) {
    res.status(401||err.status).json({statusCode : err.status, message: err.message})
  }
};

// 회원가입 시 인증 번호 확인
verify = async(req, res, next) => {
  try {
    const phoneNumber = req.body.phoneNumber;
    const verifyCode = req.body.verifyCode;

    const verify = await this.smsService.verify(phoneNumber, verifyCode)
  
    res.status(201).json(verify)
  } catch(err) {
    res.status(401).json({statusCode : err.status, message: err.message})
  }

};

// 아이디 찾기 할때 사용하는 인증번호 보내기
sendID = async (req, res, next) => {
  try {
    const phoneNumber = req.body.phoneNumber;
  
    const sendID = await this.smsService.sendID(phoneNumber);

    res.status(201).json({code: 201, message: "본인인증 문자 발송 성공", verifyCode: sendID})
  } catch(err) {
    res.status(401).json({statusCode : err.status, message: err.message})
  }
};

// 아이디 찾을 때 인증번호 받은거 확인
verifyID = async (req, res, next) => {
  try {
    const phoneNumber = req.body.phoneNumber;
    const verifyCode = req.body.verifyCode;
  
    const verifyID = await this.smsService.verifyID(phoneNumber, verifyCode)
    
    res.status(201).json(verifyID)
  } catch(err) {
    res.status(401).json({statusCode : err.status, message: err.message})
  }
};

// 비밀번호 찾기 할때 사용하는 인증번호 보내기
sendPW = async (req, res, next) => {
  try {
    const userId = req.body.userId;
    const phoneNumber = req.body.phoneNumber;

    const sendPW = await this.smsService.sendPW(phoneNumber, userId)

    res.status(201).json({code: 201, message: "본인인증 문자 발송 성공", verifyCode: sendPW})
  } catch(err) {
    res.status(401).json({statusCode : err.status, message: err.message})
  }
};
}

module.exports = SMS;
// module.exports = { send, verify, sendID, verifyID, sendPW };


services/sms.js

더보기
require("dotenv").config();
const axios = require('axios');
const CryptoJS = require('crypto-js');
const Cache = require('memory-cache');
const SmsRepository = require("../repositories/sms");

class SmsService {
  smsRepository = new SmsRepository();

  send = async (phoneNumber) => {
    const date = Date.now().toString();
    const uri = process.env.SENS_SERVICE_ID
    const secretKey = process.env.SENS_SERVICE_SECRET_KEY
    const accessKey = process.env.SENS_SERVICE_ACCESS_KEY
    const method = 'POST';
    const space = " ";
    const newLine = "\n";
    const url = `https://sens.apigw.ntruss.com/sms/v2/services/${uri}/messages`;
    const url2 = `/sms/v2/services/${uri}/messages`;

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

    hmac.update(method);
    hmac.update(space);
    hmac.update(url2);
    hmac.update(newLine);
    hmac.update(date);
    hmac.update(newLine);
    hmac.update(accessKey);

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

    const verifyCode = Math.floor(Math.random() * (999999 - 100000)) + 100000;

    Cache.del(phoneNumber);

    Cache.put(phoneNumber, verifyCode.toString());

    const findPhone = await this.smsRepository.findPhone(phoneNumber)
    if (findPhone) {
      const err = new Error(`SmsService Error`);
      err.status = 999;
      err.message = "아이디는 핸드폰 번호당 1개만 사용가능합니다.";
      throw err;
    }

    await this.smsRepository.UpdateCode(phoneNumber, verifyCode);

    axios({
      method: method,
      json: true,
      url: url,
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'x-ncp-iam-access-key': accessKey,
        'x-ncp-apigw-timestamp': date,
        'x-ncp-apigw-signature-v2': signature,
      },
      data: {
        type: 'SMS',
        contentType: 'COMM',
        countryCode: '82',
        from: process.env.SENS_MY_NUM,
        content: `[Board With] 인증번호 [${verifyCode}]를 입력해주세요.`,
        messages: [
          {
            to: `${phoneNumber}`,
          },
        ],
      }
    }).then(function (res) {
      console.log('response', res.data, res['data']);
      return verifyCode;
    })
      .catch((err) => {
        if (err.res == undefined) {
          return verifyCode;
        }
      })
      return verifyCode;
  }

  verify = async (phoneNumber, verifyCode) => {
    const CacheData = Cache.get(phoneNumber);
    if (CacheData !== verifyCode) {
      const err = new Error(`SmsService Error`);
      err.status = 401;
      err.message = "인증번호가 틀렸습니다.";
      throw err;
    } else {
      Cache.del(phoneNumber);
      return 'success';
    }
  }

  sendID = async (phoneNumber) => {
    const date = Date.now().toString();
    const uri = process.env.SENS_SERVICE_ID
    const secretKey = process.env.SENS_SERVICE_SECRET_KEY
    const accessKey = process.env.SENS_SERVICE_ACCESS_KEY
    const method = 'POST';
    const space = " ";
    const newLine = "\n";
    const url = `https://sens.apigw.ntruss.com/sms/v2/services/${uri}/messages`;
    const url2 = `/sms/v2/services/${uri}/messages`;

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

    hmac.update(method);
    hmac.update(space);
    hmac.update(url2);
    hmac.update(newLine);
    hmac.update(date);
    hmac.update(newLine);
    hmac.update(accessKey);

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

    const verifyCode = Math.floor(Math.random() * (999999 - 100000)) + 100000;

    const findPhone = await this.smsRepository.findPhone(phoneNumber)
    if (!findPhone) {
      const err = new Error(`SmsService Error`);
      err.status = 401;
      err.message = "일치하는 핸드폰 번호가 없습니다.";
      throw err;
    }

    await this.smsRepository.UpdateCode(phoneNumber, verifyCode);

    axios({
      method: method,
      json: true,
      url: url,
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'x-ncp-iam-access-key': accessKey,
        'x-ncp-apigw-timestamp': date,
        'x-ncp-apigw-signature-v2': signature,
      },
      data: {
        type: 'SMS',
        contentType: 'COMM',
        countryCode: '82',
        from: process.env.SENS_MY_NUM,
        content: `[Board With] 인증번호 [${verifyCode}]를 입력해주세요.`,
        messages: [
          {
            to: `${phoneNumber}`,
          },
        ],
      }
    }).then(function (res) {
      console.log('response', res.data, res['data']);
      return verifyCode;
    })
      .catch((err) => {
        if (err.res == undefined) {
          return verifyCode;
        }
      })
      return verifyCode;
  }

  verifyID = async (phoneNumber, verifyCode) => {
    const findValue = await this.smsRepository.findValue(phoneNumber, verifyCode);
    if (!findValue) {
      const err = new Error(`SmsService Error`);
      err.status = 401;
      err.message = "인증번호가 틀렸습니다.";
      throw err;
    } else if (findValue.verifyCode !== verifyCode) {
      const err = new Error(`SmsService Error`);
      err.status = 401;
      err.message = "인증번호가 틀렸습니다.";
      throw err;
    } else {
      return findValue.userId;
    }
  }

  sendPW = async (phoneNumber, userId) => {
    const date = Date.now().toString();
    const uri = process.env.SENS_SERVICE_ID
    const secretKey = process.env.SENS_SERVICE_SECRET_KEY
    const accessKey = process.env.SENS_SERVICE_ACCESS_KEY
    const method = 'POST';
    const space = " ";
    const newLine = "\n";
    const url = `https://sens.apigw.ntruss.com/sms/v2/services/${uri}/messages`;
    const url2 = `/sms/v2/services/${uri}/messages`;

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

    hmac.update(method);
    hmac.update(space);
    hmac.update(url2);
    hmac.update(newLine);
    hmac.update(date);
    hmac.update(newLine);
    hmac.update(accessKey);

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

    const verifyCode = Math.floor(Math.random() * (999999 - 100000)) + 100000;

    const findPass = await this.smsRepository.findPass(phoneNumber, userId)
    if (!findPass) {
      const err = new Error(`SmsService Error`);
      err.status = 401;
      err.message = "핸드폰 번호 혹은 아이디가 일치하지 않습니다.";
      throw err;
    }

    await this.smsRepository.UpdateCode(phoneNumber, verifyCode);

    axios({
      method: method,
      json: true,
      url: url,
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
        'x-ncp-iam-access-key': accessKey,
        'x-ncp-apigw-timestamp': date,
        'x-ncp-apigw-signature-v2': signature,
      },
      data: {
        type: 'SMS',
        contentType: 'COMM',
        countryCode: '82',
        from: process.env.SENS_MY_NUM,
        content: `[Board With] 인증번호 [${verifyCode}]를 입력해주세요.`,
        messages: [
          {
            to: `${phoneNumber}`,
          },
        ],
      }
    }).then(function (res) {
      console.log('response', res.data, res['data']);
      return verifyCode;
    })
      .catch((err) => {
        if (err.res == undefined) {
          return verifyCode;
        }
      })
      return verifyCode;
  }
}

module.exports = SmsService;


repositories/sms.js

더보기
const Users = require("../schema/users")

class SmsRepository {
  UpdateCode = async(phoneNumber, verifyCode) => {
    const UpdateCode = await Users.updateOne({phoneNumber : phoneNumber},{$set : {verifyCode : verifyCode}})
    return UpdateCode;
  }

  findPhone = async(phoneNumber) => {
    const findPhone = await Users.findOne({phoneNumber : phoneNumber});
    return findPhone;
  }

  findPass = async(phoneNumber, userId) => {
    const findPass = await Users.findOne({phoneNumber : phoneNumber, userId : userId});
    return findPass;
  }

  findValue = async(phoneNumber, verifyCode) => {
    const findValue = await Users.findOne({phoneNumber : phoneNumber, verifyCode : verifyCode})
    return findValue;
  }
}

module.exports = SmsRepository;