일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 코딩테스트
- TiL
- 개발자취업
- CSS
- 중간이들
- 방송대컴퓨터과학과
- mongoDB
- 방송대
- nestjs
- 엘리스sw트랙
- 코드잇
- HTML
- 파이썬
- JavaScript
- 99클럽
- node.js
- Cookie
- 꿀단집
- Git
- 코딩테스트준비
- SQL
- redis
- 데이터베이스시스템
- 유노코딩
- 파이썬프로그래밍기초
- aws
- 항해99
- Python
- 자격증
- 프로그래머스
- Today
- Total
배꼽파지 않도록 잘 개발해요
[중간이들] Twilio SMS 서비스 구현 과정과 오류 처리 본문
https://programming-bellybutton.tistory.com/211
[중간이들] SMS 인증번호 발송서비스 플랫폼으로 네이버 클라우드를 선택하지 않은 이유
중간이들 커뮤니티 사이트에서는 SMS로 인증번호 6자리를 발송하는 API가 있다. 이를 위해 twilio와 네이버 클라우드 SMS 서비스 둘 중 한 곳을 플랫폼으로 선택하기로 하였다. 결론부
programming-bellybutton.tistory.com
네이버 클라우드 SMS 서비스는 사업자등록증이 없어서 못하고,
해외 SMS 서비스 중 Twilio라는 회사의 서비스가 있어서 사용하게 되었다.
트윌리오에 가입 후 메인 화면을 보면 SMS를 발송할 수 있는 서비스는 크게 2개가 있다.
- Verify : 메시지에 인증번호만 보내는 서비스
- Messaging : 인증번호 외 본문을 커스텀해서 메시지를 보낼 수 있는 서비스
둘 다 사용하기 편하지만 인증번호만 전송할 거라면 Verify 서비스가 더 간편하다.
Twilio SMS 서비스는 '무료'임을 자처하고 있다.
API 키를 발급받아 직접 구현해본 사람으로서 이게 틀린 소리는 아니지만 굉장히 어이가 없는 말이다.
트윌리오가 가입시 제공해주는 가상 휴대폰번호로, 등록된 수신자에게만 무료 SMS 서비스가 가능한 것이었다.
verified callers 목록에 문자를 받을 수 있는 전화번호들을 등록해주면 해당 번호로는 정상적으로 발신이 되어야 한다.
하지만 나의 경우에는 verified callers로 등록되어 있는 번호에 전송을 하였음에도 계속 트윌리오 서버에서 에러를 던져주었다.
- 트윌리오가 발급해준 가상 번호 (발신자) => 트윌리오의 테스트용 가상 번호 (수신자) : 전송 O
- 트윌리오가 발급해준 가상 번호 (발신자) => 실제 내 휴대폰번호 (수신자) : 전송 X, 에러
맨 처음에는 Verify가 아닌 SMS 서비스를 사용했는데, 이런 문제가 발생하니까 내 서버 코드가 문제인가 싶어서 Verify로 구현을 하였다.
https://www.peterkimzz.com/phone-validation-service-twilio-in-5-minutes/
Twilio 번호 구매 없이 연락처 인증 서비스 5분만에 구현하기
이번 포스팅에선 Twilio를 이용해 Node.js에서 개인 번호를 발급받지 않고, 핸드폰 번호 인증을 매우 간단하게 구현하는 방법에 대해 소개해드리겠습니다.
www.peterkimzz.com
https://www.twilio.com/docs/verify/api/verification
Verifications | Twilio
The Twilio Verify REST API allows you to verify that a user has a claimed device, phone number, or email address in their possession. The API lets you start a new verification for a user, and then check that the verification was successful. Encoding type:a
www.twilio.com
위 두 개의 링크를 참고해서 작성을 하면 된다.
공식 문서도 잘 되어 있고, 해외 소스들도 있어서 구현은 어렵지 않다.
휴대폰번호는 010-1234-5678이라면 +821012345678 형태로 처리하여 발송해야한다.
그런데 에러처리가 문제였다.
제대로 발급된 번호를 입력하면 트윌리오 서버에서 인증 결과를 response로 전달한다.
이 phone/verification은 request body로 휴대폰번호와 인증번호가 들어간다.
휴대폰번호는 제대로 입력하고 인증번호를 제대로 입력하지 않은 경우, 원래는 에러가 나야 한다.
하지만 Status가 200인 response가 돌아왔다.
트윌리오에서 인증번호가 일치하지 않으면 알아서 에러를 터뜨려 주는 줄 알았는데, 그게 아니었다.
만약 인증이 실패한 경우에는 오류가 나는게 아니라 valid가 false가 된다.
그래서 클라이언트측에서 발생할 수 있는 에러 상황을 다 모아서 처리하였다.
아래 20404는 인증번호 발신 내역을 찾을 수 없다는 에러이다.
기본적으로 인증번호 발신 내역은 10분 동안 저장된다.
10분이 지나면 인증이 만료되어 저렇게 내역이 없다는 오류가 난다.
그리고 최대 시도횟수는 5회이다.
다섯 번째 연속 에러가 나면 한동안 같은 휴대폰번호로는 인증이 불가능하다.
몇 분 기다린 후 해당 휴대폰번호로 인증번호를 재발급하면 된다. 최대 시도횟수 초과 이후 정확히 몇 분을 기다려야 하는지는 알아보지 못했다.
이런 상황을 종합적으로 고려햐여 서버에서 직접 에러처리를 해줘야 한다.
결론
- Twilio SMS 서비스 중 인증번호만 사용해야할 경우 Verifiy 서비스를 추천함.
- 실서비스화하려면 첫 결제시 약 3만 원치의 금액을 납부해야 함. 그래야 자유롭게 어떤 번호든 인증 번호가 발송됨.
- 인증 실패의 경우 "valid": false로 응답이 오기 때문에, 각 케이스별 에러는 개발자가 직접 처리해주어야 함.
- 기본 설정은 발신내역 10분간 저장, 최대 시도횟수 5회임.
auth.service.ts
// 휴대폰 번호 인증 확인
async verifyPhoneNumberCode(to: string, code: string) {
return await this.authTwilioService.checkVerificationCode({ to, code });
}
auth.twilio.service.ts
import * as twilio from 'twilio';
export class AuthTwilioService {
private client: twilio.Twilio;
private accountSid = process.env.TWILIO_ACCOUNT_SID;
private authToken = process.env.TWILIO_AUTH_TOKEN;
private verifyServiceSid = process.env.TWILIO_SERVICE_SID;
constructor() {
this.client = twilio(this.accountSid, this.authToken);
}
// 인증번호를 담은 메시지 보내기
sendVerificationCode(options: { to: string }) {
return this.client.verify.v2
.services(this.verifyServiceSid)
.verifications.create({ to: options.to, channel: 'sms' });
}
// 전송한 메시지 인증번호 확인하기
async checkVerificationCode(options: { to: string; code: string }) {
const result = await this.client.verify.v2.services(this.verifyServiceSid).verificationChecks.create({
to: options.to,
code: options.code,
});
return result;
}
}
auth.controller.ts
// 휴대폰 인증번호 발급
@Post('phone')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '휴대폰 인증번호 발급' })
@ApiBody({
type: VerifyPhoneNumberDto,
description: '휴대폰 인증번호 발급을 위한 정보',
examples: {
'example-1': {
summary: '휴대폰 인증번호 발급 예시',
value: {
phoneNumber: '01012341234',
},
},
},
})
@ApiResponse({
status: 200,
description: '휴대폰 인증번호 발급이 성공하였습니다.',
schema: {
example: {
message: '휴대폰 인증번호 발급이 성공하였습니다.',
},
},
})
@ApiResponse({ status: 400, description: '잘못된 요청' })
async postPhoneVerificationCode(@Body() sendPhoneVerificationDto: SendPhoneVerificationDto) {
const { phoneNumber } = sendPhoneVerificationDto;
await this.authService.sendPhoneVerificationCode(phoneNumber);
return { message: '휴대폰 인증번호가 발급되었습니다.' };
}
auth.controller.ts
// 휴대폰 인증번호 발급 확인
@Post('phone-verification')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: '휴대폰 인증번호 발급 확인' })
@ApiBody({
type: VerifyPhoneNumberDto,
description: '휴대폰 인증번호 확인을 위한 정보',
examples: {
'example-1': {
summary: '휴대폰 인증번호 발급 확인 예시',
value: {
phoneNumber: '01012341234',
code: '654321',
},
},
},
})
@ApiResponse({
status: 200,
description: '휴대폰 인증이 성공하였습니다.',
schema: {
example: {
message: '휴대폰 인증이 성공하였습니다.',
},
},
})
@ApiResponse({
status: 400,
description: '잘못된 전화번호 형식입니다. 전화번호는 01012345678과 같은 11자리 숫자로 이루어져야 합니다.',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: '잘못된 전화번호 형식입니다. 전화번호는 01012345678과 같은 11자리 숫자로 이루어져야 합니다.',
},
error: { type: 'string', example: 'Invalid PhoneNumber Request Type' },
statusCode: { type: 'number', example: 400 },
},
},
})
@ApiResponse({
status: 403,
description: '잘못된 인증 코드입니다. 입력한 코드를 확인해주세요.',
schema: {
type: 'object',
properties: {
message: { type: 'string', example: '잘못된 인증 코드입니다. 입력한 코드를 확인해주세요.' },
error: { type: 'string', example: 'Invalid Verification Code' },
statusCode: { type: 'number', example: 403 },
},
},
})
@ApiResponse({
status: 400,
description: '해당 전화번호에 대한 인증 내역이 없습니다. 인증이 없거나 유효 기간이 지났습니다.',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: '해당 전화번호에 대한 인증 내역이 없습니다. 인증이 없거나 유효 기간이 지났습니다.',
},
error: { type: 'string', example: 'No Verification Record' },
statusCode: { type: 'number', example: 400 },
},
},
})
@ApiResponse({
status: 409,
description: '이미 인증이 완료된 전화번호입니다. 추가 인증이 필요하지 않습니다.',
schema: {
type: 'object',
properties: {
message: { type: 'string', example: '이미 인증이 완료된 전화번호입니다. 추가 인증이 필요하지 않습니다.' },
error: { type: 'string', example: 'Already Verified' },
statusCode: { type: 'number', example: 409 },
},
},
})
@ApiResponse({
status: 429,
description: '최대 인증 시도 횟수(5회)에 도달했습니다. 인증을 처음부터 다시 시도해주세요.',
schema: {
type: 'object',
properties: {
message: {
type: 'string',
example: '최대 인증 시도 횟수(5회)에 도달했습니다. 인증을 처음부터 다시 시도해주세요.',
},
error: { type: 'string', example: 'Max Check Attempts Reached' },
statusCode: { type: 'number', example: 429 },
},
},
})
@ApiResponse({
status: 500,
description: '서버에서 오류가 발생했습니다.',
schema: {
example: {
message: '서버에서 오류가 발생했습니다.',
error: 'Internal Server Error',
statusCode: 500,
},
},
})
async postPhoneVerificationConfirm(@Body() verifyPhoneNumberDto: VerifyPhoneNumberDto) {
const { phoneNumber, code } = verifyPhoneNumberDto;
const formattedPhoneNumber = formattingPhoneNumber(phoneNumber);
try {
const result = await this.authService.verifyPhoneNumberCode(formattedPhoneNumber, code);
if (!result.valid) {
throw new InvalidPhoneVerificationCodeException();
}
return { message: '휴대폰 인증이 성공하였습니다.' };
} catch (error) {
handlePostPhoneVerificationConfirmError(error);
}
}
handle-post-phone-verification-confirm-error.ts
import { InternalServerErrorException } from '@nestjs/common';
import {
InvalidPhoneVerificationCodeException,
NoPhoneVerificationRecordException,
MaxCheckAttemptsException,
InvalidPhoneNumberException,
} from 'src/common/exceptions/twilio-sms.exceptions';
export function handlePostPhoneVerificationConfirmError(error: any): void {
console.error('Error: ', error);
// 입력한 전화번호 형식이 유효하지 않은 경우
if (error.code === 60200) {
throw new InvalidPhoneNumberException();
}
// 인증번호가 불일치할 때
if (error instanceof InvalidPhoneVerificationCodeException) {
throw new InvalidPhoneVerificationCodeException();
}
// 해당 번호에 대한 인증내역이 없는 경우
if (error.code === 20404) {
throw new NoPhoneVerificationRecordException();
}
// 최대 인증 횟수에 도달한 경우
if (error.code === 60202) {
throw new MaxCheckAttemptsException();
}
// 그 외의 예외는 Internal Server Error로 처리
throw new InternalServerErrorException('서버에서 오류가 발생했습니다.');
}
'Project' 카테고리의 다른 글
[중간이들] URL 생성할 때는 항상 슬래시 (경로 구분자)를 조심해야함 (0) | 2024.10.04 |
---|---|
[중간이들] AWS S3 presigned-url content type 불일치 오류 해결 (0) | 2024.10.04 |
[중간이들] NestJS 세션 로그아웃 구현 중 쿠키 문제 해결 과정 (0) | 2024.09.13 |
[중간이들] SMS 인증번호 발송서비스 플랫폼으로 네이버 클라우드를 선택하지 않은 이유 (0) | 2024.09.10 |
[꿀단집] 구글 OAuth 소셜 로그인: 심사 통과 후 프로덕션 환경에서 외부인 로그인 가능 (2) | 2024.09.10 |