그저 내가 되었고
🎯Node.JS:: 네이버 클라우드 플랫폼; 본인 인증(SMS 인증) Rest API 구현하기 본문
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;
'개발 > Node.js' 카테고리의 다른 글
🎯Node.js + Express:: req.params vs. req.query vs. req.body (0) | 2022.12.11 |
---|---|
🎯Node.JS + OAuth:: 소셜로그인(구글, 카카오, 네이버) 백엔드 코드 뜯어보기 (0) | 2022.12.06 |
🎯Node.js + Oauth:: 네이버 로그인 구현하기(w/o passport) (0) | 2022.12.02 |
🎯Node.js + Oauth:: 구글 로그인 개념(w/o passport) (0) | 2022.11.28 |
🎯Node.js + Express:: 실시간 채팅 w/ Users & Rooms (- Ban & Kickout) (0) | 2022.11.24 |