전화번호 점유 인증을 위해 sms 발송에는 coolSms, 내부 인증 절차에는 redis 를 이용하기로 했다.
CoolSms 에서 API Key, Secret Key, 발송 번호를 등록한 후,
JAVA SDK 를 사용하여 연동하였다.
SDK 문서에 예제가 잘 작성되어 있어, 해당 문서를 참고하여 연동 및 구현하였다.
sms:
key: api_key
secret: secret_key
send-number: send_number
@Service
@Slf4j
public class SmsService {
private static final String DOMAIN = "https://api.coolsms.co.kr";
// 전화번호 인증 코드 발송시 사용할 텍스트
private static final String PHONE_AUTH_TEXT = "[date-plan] 휴대전화 인증을 위한 인증 코드입니다. \n %s";
// 성공 코드
private static final Set<String> SUCCESS_CODE = Set.of("2000", "3000", "4000");
private final DefaultMessageService messageService;
private final String sendNumber;
public SmsService(@Value("${sms.key}") String key,
@Value("${sms.secret}") String secret,
@Value("${sms.send-number}") String sendNumber) {
this.messageService = NurigoApp.INSTANCE.initialize(key, secret, DOMAIN);
this.sendNumber = sendNumber;
}
public void sendSmsForPhoneAuthentication(String toNumber, int code) {
Message message = createMessage(toNumber, code);
SingleMessageSentResponse response = this.messageService.sendOne(
new SingleMessageSendingRequest(message));
boolean success = isSuccess(response);
if (!success) {
throw new SmsSendFailException(SmsType.PHONE_AUTHENTICATION);
}
}
// 성공 여부 판단
// 위에서 작성한 성공 코드에 포함되지 않는다면 false 리턴한다.
private boolean isSuccess(SingleMessageSentResponse response) {
return response != null && SUCCESS_CODE.contains(response.getStatusCode());
}
// 메시지 생성 및 설정
private Message createMessage(String toNumber, int code) {
Message message = new Message();
message.setFrom(sendNumber);
message.setTo(toNumber);
message.setText(String.format(PHONE_AUTH_TEXT, code));
return message;
}
}
구현 내용을 살펴보기 전에, Redis 를 사용한 이유는 다음과 같다.
또한, 이후의 회원가입시에 특정 전화번호가 인증되어 있는지 확인 후 가입시켜야 하므로 redis 의 자료구조를 List 로 하여 다음과 같이 저장했다.
key | index0 | index1 |
---|---|---|
전화번호 | 인증 코드 | 인증 상태 |
010XXXXXXXX | 123456 | true, false |
@Service
@RequiredArgsConstructor
public class AuthService {
private static final String AUTH_KEY_PREFIX = "[AUTH]";
private final MemberReadService memberReadService;
private final SmsService smsService;
private final StringRedisTemplate redisTemplate;
// sms 전송
public void sendSms(PhoneServiceRequest request) {
// 랜덤 코드 생성
int code = RandomCodeGenerator.generateCode(6);
String phone = request.getPhone();
// 이미 가입된 전화번호라면 예외 발생
memberReadService.throwIfPhoneExists(phone);
smsService.sendSmsForPhoneAuthentication(phone, code);
saveAuthCodeInRedis(phone, code);
}
public void authenticateAuthCode(PhoneAuthCodeServiceRequest request) {
ListOperations<String, String> opsForList = redisTemplate.opsForList();
String phone = request.getPhone();
String key = AUTH_KEY_PREFIX + phone;
String savedCode = opsForList.index(key, 0);
validateCode(savedCode, request.getCode());
// 인증상태를 true 로 변경
opsForList.set(key, 1, Boolean.TRUE.toString());
}
// 전화번호, 인증코드, 인증상태를 redis 에 저장
private void saveAuthCodeInRedis(String phone, int code) {
ListOperations<String, String> opsForList = redisTemplate.opsForList();
String key = AUTH_KEY_PREFIX + phone;
// 기존에 존재하던 인증정보 삭제
redisTemplate.delete(key);
// 인증 정보 저장
opsForList.rightPush(key, String.valueOf(code));
opsForList.rightPush(key, String.valueOf(false));
redisTemplate.expire(key, 2, TimeUnit.MINUTES);
}
private void validateCode(String code, String input) {
if (code == null || !Objects.equals(input, code)) {
throw new InvalidPhoneAuthCodeException(code);
}
}
}