[Spring] 전화번호 인증 구현

김현진·2023년 5월 23일
0

커플 SNS 프로젝트

목록 보기
3/4

전화번호 인증

전화번호 점유 인증을 위해 sms 발송에는 coolSms, 내부 인증 절차에는 redis 를 이용하기로 했다.


CoolSms 연동

CoolSms 에서 API Key, Secret Key, 발송 번호를 등록한 후,
JAVA SDK 를 사용하여 연동하였다.
SDK 문서에 예제가 잘 작성되어 있어, 해당 문서를 참고하여 연동 및 구현하였다.

.yml 파일에 Api Key, Secret Key, 발송 번호 작성

sms: 
 key: api_key
 secret: secret_key
 send-number: send_number

Service 작성

@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 ttl 사용

또한, 이후의 회원가입시에 특정 전화번호가 인증되어 있는지 확인 후 가입시켜야 하므로 redis 의 자료구조를 List 로 하여 다음과 같이 저장했다.

keyindex0index1
전화번호인증 코드인증 상태
010XXXXXXXX123456true, false

전화번호 인증 프로세스

  1. 사용자가 전화번호를 입력한다.
  2. 인증 코드를 생성하고, sms 를 발송하고, redis 에 전화번호, 인증코드, 인증 상태(false)을 저장한다.
  3. 사용자가 인증 코드를 입력한다.
  4. redis 에 저장되어 있는 전화번호, 코드를 확인 후 일치하면 인증 상태를 true로 변경한다.
  5. 이후의 회원가입시, 인증 상태를 확인 후 회원가입을 진행한다.

구현

@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);
		}
	}
}

0개의 댓글