https://coolsms.co.kr/ : 건당 20원, 최초 가입 시 300P 무료 제공!
회원가입을 구현하다 보니 자연스럽게 인증 로직을 구현하게 되었는데, 이메일이랑 휴대폰 문자 인증 둘 중에 고민하다가 이메일 계정 무한 생성 << 같은 부정행위를 막기 위해서 한 사람당 한 번만 가입할 수 있도록 문자 인증을 도입하기로 결정하게 되었다!
이후에 관련 API를 찾아보다 가장 많은 샘플이 존재했고, 또 팀원이 이전 팀에서 써본 경험이 있었던 CoolSMS를 사용하기로 결정. 유료이긴 했으나 무료 포인트를 주니까 테스트용으로 충분할 것 같아서 걍 사용,, 하기로 했으나 막판에 모자라서 걍 엔빵해서 결제하긴함,,ㅋㅋ
발급 : 회원가입 후 대시보드 ➡️ 개발/연동 ➡️ API Key 발급 ➡️ 새로운 API KEY
Spring 연동 : 공식 깃허브 참고!!
CoolSMS에서 공식 깃허브를 운영하면서 언어 + 프레임워크별 의존성 및 샘플 코드를 제공해준다! 나는 Gradle 기반의 Spring Boot를 사용하므로 해당 레포를 참고하여 build.gradle
에 아래 의존성을 추가해줬다.
implementation 'net.nurigo:sdk:4.3.0'
추가적으로 application.yaml
에 위에서 발급했던 API Key와 Secret Key, 그리고 문자 전송 시 사용할 번호 데이터를 미리 입력해둠!
coolsms:
api:
key: "API KEY 입력"
secret: "SECRET KEY 입력"
sender: "발신자 번호" # (- 없어야 함)
로직의 큰 틀은 문자 인증 요청 ➡️ 문자 발송 ➡️ 인증번호 검증인데, 이 과정에서 인증번호는 5분동안만 유효하도록! 구현하는 것이 목표였음..
문제는 인증번호를 5분동안 어디에 저장하냐?였는데 MySQL은 서버 부하때문에 제외였고,, 선택지는 세션 아니면 레디스였는데 이것저것 찾아보니 세션의 time-out 기능을 사용하게 되면 세션을 사용하는 다른 로직에 영향을 미칠 수 있다는 점과, 세션 키가 중복될 위험이 있다는 점이 걸려서 처리 속도가 빠른 인메모리 캐시 DB인 Redis를 사용하기로 했다! 따라서,
문자 인증 요청 ➡️ 인증번호 발급 후 Redis에
<휴대폰번호, 인증번호>
저장 (만료시간 지정) ➡️ 검증 요청 시 키 값인 번호로 조회 ➡️ 검증 통과 시 데이터 삭제
이 로직을 최종적인 틀로 잡고 구현 시작,,
SmsUtil
: CoolSMS에서 제공하는 DefaultMessageService
와 SingleMessageSentResponse
객체를 사용하여, 인증번호와 수신자 번호를 전달받으면 실제 메세지를 생성하여 전송하는 로직 구현@Component
public class SmsUtil {
@Value("${coolsms.api.key}")
private String apiKey;
@Value("${coolsms.api.secret}")
private String secretKey;
@Value("${coolsms.api.sender}")
private String sender;
DefaultMessageService messageService;
@PostConstruct
private void init() {
this.messageService = NurigoApp.INSTANCE.initialize(apiKey, secretKey, "https://api.coolsms.co.kr");
}
// 인증번호 전송 : 단일 메세지
public SingleMessageSentResponse sendSms(String receiver, String code) {
Message message = new Message();
message.setFrom(sender); // 보내는 사람 : 관리자
message.setTo(receiver); // 받는 사람 : 유저
message.setText("[MFC] 본인 확인 인증번호 : [" + code + "]\n인증번호는 5분간 유효합니다."); // 전송 내용
return messageService.sendOne(new SingleMessageSendingRequest(message));
}
}
AuthService
: 인증을 요청한 휴대폰 번호를 통해 해당 회원이 이미 존재하는지 검증하고, 새로 가입하는 회원인 경우 6자리의 인증번호 code
를 발급하여 SmsUtil
의 sendSms()
를 호출하여 메세지를 전송한 후, redis에 해당 내역을 저장@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {
private final SmsRepository smsRepository;
private final SmsUtil smsUtil;
@Override
public void sendSms(SmsReqDto dto) {
String to = dto.getPhone();
if(isDuplicate(to)) {
throw new BaseException(DUPLICATED_MEMBERS);
}
int random = (int) (Math.random() * 900000) + 1000;
String code = String.valueOf(random);
smsUtil.sendSms(to, code); // 문자 전송
smsRepository.createSmsCode(to, code); // 인증번호 redis에 저장
}
private boolean isDuplicate(String phone) {
return memberRepository.findByActivePhone(phone).isPresent();
}
}
SmsRepository
: redis에 <휴대폰번호, 인증번호>
형식으로 인증 내역을 저장하는데, 이때 Duration
을 통해 해당 데이터의 유효기간을 지정할 수 있음. 인증번호가 5분동안만 유효하므로 LIMIT_TIME
을 5분으로 설정해줌.@Repository
@RequiredArgsConstructor
public class SmsRepository {
private final StringRedisTemplate redisTemplate;
private final String PREFIX = "sms:";
private final int LIMIT_TIME = 5 * 60;
public void createSmsCode(String phone, String code) {
redisTemplate.opsForValue()
.set(PREFIX + phone, code, Duration.ofSeconds(LIMIT_TIME));
}
}
AuthService
: redis에서 휴대폰 번호를 통해 값을 조회해서 인증번호를 검증한 후, 통과했다면 해당 인증번호는 더이상 의미없기 때문에 redis에서 삭제 처리.@Service
@RequiredArgsConstructor
@Transactional
public class AuthServiceImpl implements AuthService {
private final SmsRepository smsRepository;
private final SmsUtil smsUtil;
@Override
public void verifyCode(SmsReqDto dto) {
if(!isVerify(dto)) {
throw new BaseException(MESSAGE_VALID_FAILED);
}
smsRepository.removeSmsCode(dto.getPhone());
}
private boolean isVerify(SmsReqDto dto) {
return smsRepository.hasKey(dto.getPhone()) &&
smsRepository.getSmsCode(dto.getPhone())
.equals(dto.getCode());
}
}
SmsRepository
hasKey(phone)
: 해당 휴대폰 번호를 키로 가지는 값이 있는지 조회. 만약 인증 요청 후 5분이 지났다면, 해당 데이터는 삭제되어 더이상 존재하지 않음.
getSmsCode(phone)
: 해당 휴대폰 번호를 키값으로 가지는 인증번호 값 조회. 사용자가 입력한 인증번호와 일치하는지 검증할 때 사용
removeSmsCode(phone)
: 해당 휴대폰 번호를 키값으로 가지는 내역 삭제
@Repository
@RequiredArgsConstructor
public class SmsRepository {
private final StringRedisTemplate redisTemplate;
private final String PREFIX = "sms:";
private final int LIMIT_TIME = 5 * 60;
public boolean hasKey(String phone) {
return redisTemplate.hasKey(PREFIX + phone);
}
public String getSmsCode(String phone) {
return redisTemplate.opsForValue().get(PREFIX + phone);
}
public void removeSmsCode(String phone) {
redisTemplate.delete(PREFIX + phone);
}
}
2-1. 잘못된 인증번호로 검증 : 예외 발생
2-2. 올바른 인증번호로 검증
2-3. 올바른 인증번호로 재검증 : 첫번째 검증 통과 시 삭제되므로, 해당 내역이 존재하지 않아 예외 발생
사실 문자 인증 처음에 담당하게 됐을 때 좀 쫄았었는데 공식 깃허브랑 몇몇 기술 블로그들이 넘 친절해서 금방 할 수 있었던 것 같다,, 나중에 개인 플젝 작은거나 하나 하면서 이메일 인증도 구현해봐야지,, 시간이 된다면,,