[Spring Boot] CoolSMS로 문자 인증 구현하기

진예·2024년 10월 1일
0

Code

목록 보기
4/5
post-thumbnail
post-custom-banner

✉️ CoolSMS

https://coolsms.co.kr/ : 건당 20원, 최초 가입 시 300P 무료 제공!

회원가입을 구현하다 보니 자연스럽게 인증 로직을 구현하게 되었는데, 이메일이랑 휴대폰 문자 인증 둘 중에 고민하다가 이메일 계정 무한 생성 << 같은 부정행위를 막기 위해서 한 사람당 한 번만 가입할 수 있도록 문자 인증을 도입하기로 결정하게 되었다!

이후에 관련 API를 찾아보다 가장 많은 샘플이 존재했고, 또 팀원이 이전 팀에서 써본 경험이 있었던 CoolSMS를 사용하기로 결정. 유료이긴 했으나 무료 포인트를 주니까 테스트용으로 충분할 것 같아서 걍 사용,, 하기로 했으나 막판에 모자라서 걍 엔빵해서 결제하긴함,,ㅋㅋ


🗝️ API Key 발급 + Spring 연동

발급 : 회원가입 후 대시보드 ➡️ 개발/연동 ➡️ 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 기능을 사용하게 되면 세션을 사용하는 다른 로직에 영향을 미칠 수 있다는 점과, 세션 키가 중복될 위험이 있다는 점이 걸려서 처리 속도가 빠른 인메모리 캐시 DBRedis를 사용하기로 했다! 따라서,

문자 인증 요청 ➡️ 인증번호 발급 후 Redis에 <휴대폰번호, 인증번호> 저장 (만료시간 지정) ➡️ 검증 요청 시 키 값인 번호로 조회 ➡️ 검증 통과 시 데이터 삭제

이 로직을 최종적인 틀로 잡고 구현 시작,,


⚙️ 메세지 전송

  • SmsUtil : CoolSMS에서 제공하는 DefaultMessageServiceSingleMessageSentResponse 객체를 사용하여, 인증번호와 수신자 번호를 전달받으면 실제 메세지를 생성하여 전송하는 로직 구현
@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를 발급하여 SmsUtilsendSms()를 호출하여 메세지를 전송한 후, 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);
	}
}

✅ 테스트

  1. 휴대폰 번호로 인증 요청 : 문자가 정상적으로 수신되는 것을 확인!

2-1. 잘못된 인증번호로 검증 : 예외 발생

2-2. 올바른 인증번호로 검증

2-3. 올바른 인증번호로 재검증 : 첫번째 검증 통과 시 삭제되므로, 해당 내역이 존재하지 않아 예외 발생


사실 문자 인증 처음에 담당하게 됐을 때 좀 쫄았었는데 공식 깃허브랑 몇몇 기술 블로그들이 넘 친절해서 금방 할 수 있었던 것 같다,, 나중에 개인 플젝 작은거나 하나 하면서 이메일 인증도 구현해봐야지,, 시간이 된다면,,

profile
백엔드 개발자👩🏻‍💻가 되고 싶다
post-custom-banner

0개의 댓글